TD 2 • Typescript, TU & dates
Cartouche
| Champ | Valeur |
|---|---|
| Auteur·e | Élise |
| Édition | 2024-03-28 |
| Durée | 2 séances |
| Taille des équipes | 2~4 personnes login des auteur·rice·s dans author.txt |
| Rendu | via git, dépôt $YEAR_node_api, droits en lecture à delivery_collector |
Barème
| Critère | PA |
|---|---|
| Exo 1 : précisions de type | 3 |
| Exo 2 : généralités du JS et du TS | 4 |
| Exo 3 : fabriquer un login | 3 |
| Exo TU : tests unitaires | 3 |
| Exo 4 : générer des dates + TU | 3 |
| Exo 5 : générer des créneaux + TU | 4 |
| Malus : Coding Style (par fichier sale) | -5 |
| Malus : Dépôt Sale (par dépôt) | -5 |
Introduction
Objectif: se familiariser avec TypeScript tout en construisant la logique métier de notre projet.
Les bases de typescript
Lecture fortement conseillée : https://www.typescriptlang.org/docs/handbook/typescript-in-5-minutes.html
Fonctions
function add_numbers(a: number, b: number): number {
return a + b;
}
console.log(add_number(1, 2));
console.log(add_number(47, 54));
// ne compile pas
console.log(add_number("hello", 54));
On peut aussi déclarer des arrow functions, que vous verrez souvent en argument d’un appel à une autre fonction :
const arrow_func = (a: number, b: number) => a + b;
Elle ont une fonctionnalité bien pratique : elles peuvent capturer les variables de l’étendue dans laquelle elles sont créées :
function mk_afine(a: number, b: number): (x: number) => number {
return (x: number) => a * x + b;
}
let affine = mk_afine(2, 10);
console.log(affine(1));
console.log(affine(2));
console.log(affine(3));
Ainsi on peut facilement générer, renvoyer des fonctions et stocker des fonctions comme des objets.
Type d’un objet
En Typescript on se sert du mot clé interface pour définir des structures ou des fonctions qui doivent figurer sur un objet qui répond à ce type. On est pas obligés d’hériter de cette interface pour y répondre :
interface Person {
full_name: string;
nick_name: string;
dob: {
day: number;
month: number;
year: number;
};
}
let john: Person = {
name: "john",
nick_name: "papa jo",
dob: { year: 1972, day: 1, month: 5 },
};
Si on ne compte jamais se servir de notre type pour de l’héritage, on peut même le créer ainsi :
type Person = {
full_name: string;
nick_name: string;
dob: {
day: number;
month: number;
year: number;
};
};
let john: Person = {
name: "john",
nick_name: "papa jo",
dob: { year: 1972, day: 1, month: 5 },
};
On peut aller plus loin et donner un nom au type de dob :
type DateOfBirth = {
day: number;
month: number;
year: number;
};
type Person = {
full_name: string;
nick_name: string;
dob: DateOfBirth;
};
let john: Person = {
name: "john",
nick_name: "papa jo",
dob: { year: 1972, day: 1, month: 5 },
};
On peut rendre certains champs optionnels avec ?. Mettons nous ne connaissons pas le surnom de quelqu’un :
type Person = {
full_name: string;
nick_name?: string;
dob: DateOfBirth;
};
let john: Person = {
name: "john",
dob: { year: 1972, day: 1, month: 5 },
};
Cela dit, avec un champ optionnel vous devez vérifier qu’il n’est pas null ou undifined avant de vous en servir :
// avant la condition le type de nickname est "string | null"
if (john.nickname) {
// la valeur est utilisable en tant que string
if (john.nickname === "papa jo")
john.nickname = john.nickname + "hn, forgot the last two letters of his name";
} else {
// la valeur n'est pas utilisable car elle est null
// mais on peut la réassigner
john.nickname = "papa john, forgot to put in his nickname";
}
Précision de type
On peut marquer des champs et variables comme pouvant avoir un type parmi une liste, par exemple :
toto: number | number[] | string | string[]find_param: { login: string } | { idx: number }role: "admin" | "moderator" | "member" | "everyone"
Dans les deux premiers cas, avant de pouvoir faire quoi que ce soit avec la valeur contenue, on va devoir faire une précision de type, du type narrowing comme le dit la doc.
Tableaux et types intégrés
Dans le cas de number | number[] | string | string[] on peut différencier entre les tableaux et les autres types avec :
if (toto instanceof Array) {
// le type de toto est ici : "number[] | string []"
} else {
// le type ici est "number | string"
}
On peut différencier entre deux types primitifs avec typeof :
// en partant de `number | string`:
if (typeof toto === "string") {
// le type ici est string
} else {
// le type ici est number
}
Maintenant si on veut différencier entre number[] | string[] il n’y a pas de mécanisme intégré, mais on peut en écrire un, sous la forme suivante :
// If we find an element that's not a string, return false.
function is_str_array(x: any[]): x is string[] {
return !x.find((elem) => typeof elem !== "string");
}
Maintenant si on utilise cette fonction dans une condition :
// type de départ number[] | string[]
if (is_str_array(toto)) {
// ici c'est un string[]
} else {
// ici c'est un number[]
}
Interface et objets customs
Regardez comment est précisé le type d’un objet avec des champs qui peuvent servir à les différencier :
function get_user(param: { login: string } | { idx: number }) {
if ("login" in params) {
// ici le type est devenu "{ login: string }"
} else {
// ici le type est "{ idx: number }"
}
}
La précision de type est possible car il n’y a pas d’ambiguïté, si le champ login est présent le type ne peut que être { login: string }, s’il ne l’est pas il ne peut que être { idx: number }.
La fonction peut s’appeler ainsi :
get_user({ login: "amelie.oudea-castera" });
get_uset({ idx: 70 });
Exercices 1, 2, 3
Fichiers à rendre : src/exoX.ts ou X est remplacé par le numéro de l’exercice.
Exercice 1 sur les précisions de type : écrivez une fonction toto_narrow qui prend un paramètre qui peut être une chaîne de caractère, un nombre, un tableau de chaînes de caractères ou un tableau de nombres.
Si vous recevez :
- une chaîne, ajoutez y " bonjour!" et renvoyez la chaîne ;
- un nombre, ajoutez y 12 et renvoyez le ;
- un tableau de nombres, ajouter la somme à la suite du tableau et renvoyez
null; - un tableau de chaînes, ajoutez-y une chaîne qui contient “je suis une chaîne supplémentaire” et renvoyez
null.
Exercice 2 sur des généralités : écrivez :
- un type
Userqui est un objet qui contient un nom, un identifiant numérique, un login et une année de naissance ; - une fonction
users_addqui prend en paramètre un tableau deUseret tout les champs d’unUsersauf l’identifiant numérique :- la fonction doit créer un
Useret l’ajouter au tableau, en générant son identifiant numérique en trouvant le prochain nombre disponible dans le tableau ;
- la fonction doit créer un
- une fonction
users_getqui prend un tableau deUseret soit un identifiant numérique, soit un login :- elle renvoie l’utilisateur qui porte ce login/identifiant si elle le trouve ou
null;
- elle renvoie l’utilisateur qui porte ce login/identifiant si elle le trouve ou
- une fonction
users_loginsqui prend en paramètre un tableau deUseret renvoie un tableau de tous les champslogin; - une fonction
users_names_by_birth_yearqui prend en paramètre un tableau deUseret renvoie un tableau de leur noms complets et de leur année de naissance, trié par leur année de naissance.
Exercice 3 qui demande des recherches : écrivez une fonction mk_login qui prend un nom complet et le transforme en un login. Exemple “Élise Philippe” devient “elise.philippe”. La fonction doit retirer les accents et diacritiques qu’elle rencontre.
Exercice 4 • tests unitaires
Écrivez les fichiers src/exo{1,2,3}.spec.ts. Ils doivent contenir les tests unitaires pour les fonctions des fichiers sources éponymes.
Vous devez pour chaque fonction tester chaque cas demandé par le sujet.
Les dates
Natives au JS
Les objets Date sont crées selon le fuseau horaire de votre machine et stockés en UTC. Ainsi 11:24 en CEST devient 09:24 en UTC. Lorsqu’on affiche une date en JS elle s’affiche au format ISO 8601. Les chaînes de ce format qui sont exprimée en UTC finissent par Z (pour zéro).
On peut afficher directement la date ou alors récupérer une chaîne qui respecte la locale (à la fois la langue et le fuseau horaire de votre ordinateur).
let d1 = new Date(2024, 2, 2, 10, 0);
console.log(d1);
// 2024-03-02T09:00:00.000Z
console.log(d1.toLocaleString());
// 2/3/2024, 10:00:00
let d2 = new Date(2024, 3, 2, 10, 0);
console.log(d2);
// 2024-04-02T08:00:00.000Z
console.log(d2.toLocaleString());
// 2/4/2024, 10:00:00
// on peut aussi choisir la langue dans laquelle afficher la date
// ainsi que si et comment afficher certains éléments de la date
console.log(d2.toLocaleString("ja", {
weekday: "long",
year: "numeric",
month: "long",
day: "numeric",
}));
// affiche: 2024年4月9日火曜日
Bibliothèques
Javascript et NodeJS ne proposent pas vraiment d’outils pour manipuler facilement les dates, mais il existe beaucoup de bibliothèques qui offrent cette fonctionnalité :
- moment
- fait parti des meubles ;
- n’est plus en développement actif mais vous la verrez un peu partout ;
- suggèrent que vous utilisiez une autre bibliothèque
- contient les définitions de types pour votre IDE et
tsc
- Luxon
- Successeur de moment
- Objets immutables (les fonctions génèrent de nouveaux objets plutôt que de modifier ceux passés en paramètre pour éviter de faire des erreurs)
- Ne contient pas les définitions de type pour votre IDE et
tsc, il faut installer un paquet@types/luxon.
- Day.js
- Reprend l’api de moment, mais pèse beaucoup moins lourd et s’exécute plus vite
- Objets immutable (les fonctions génèrent de nouveaux objets plutôt que de modifier ceux passés en paramètre pour éviter de faire des erreurs)
- Contient les définitions de type pour votre IDE et
tsc - Très léger
- date-fns
- Rapide et
furieuxléger - Fonctionne directement avec les objets
Datenatifs - Basé sur des fonctions pures qui ne modifient pas les dates passées en paramètre mais en créent des nouvelles (permet d’éviter de faire des erreurs)
- Programmé en typescript !
- Rapide et
- JS Joda
- Ré-implémentation complète des dates
Exercices
Dans notre projet de gestion de créneaux, il y a forcément des calculs à faire sur les dates des créneaux. Dans ces exercices on va essayer de : générer des créneaux avec les bonne dates et avec quelques paramètres.
Exercice 5
Écrivez une fonction mk_dates_every_minute qui prend en paramètre une date de départ, un nombre de minutes et un nombre de dates à générer. Elle doit renvoyer un tableau de dates espacées de ce nombre de minutes. La première date du tableau doit être la date de départ passée en paramètre.
Les paramètres de votre fonction doivent être les champs d’un unique objet. Exemple :
function add(params: {a: number, b: number}): number {
return params.a + params.b;
}
Exercice 6
Créez un type TimeSlot qui contient une date de départ, une date de fin et (dans un champ optionnel) le login d’une personne inscrite au créneau.
Écrivez une fonction mk_slots qui prends les mêmes paramètres que mk_dates_every_minute. Elle doit renvoyer un tableau de TimeSlots.
Exercice 7
Ajoutez à votre fonction mk_slots les paramètres optionnels :
- pour l’espacement d’un créneau à l’autre ;
- le nombre de créneau avant une pause ;
- la durée d’une pause.
Votre fonction doit prendre en compte l’espacement entre deux créneaux s’il a une valeur.
Elle doit aussi prendre en compte les pauses et leur durée lors de la génération si les deux ont une valeur.