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 User qui est un objet qui contient un nom, un identifiant numérique, un login et une année de naissance ;
  • une fonction users_add qui prend en paramètre un tableau de User et tout les champs d’un User sauf l’identifiant numérique :
    • la fonction doit créer un User et l’ajouter au tableau, en générant son identifiant numérique en trouvant le prochain nombre disponible dans le tableau ;
  • une fonction users_get qui prend un tableau de User et soit un identifiant numérique, soit un login :
    • elle renvoie l’utilisateur qui porte ce login/identifiant si elle le trouve ou null ;
  • une fonction users_logins qui prend en paramètre un tableau de User et renvoie un tableau de tous les champs login ;
  • une fonction users_names_by_birth_year qui prend en paramètre un tableau de User et 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
  • 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 furieux léger
    • Fonctionne directement avec les objets Date natifs
    • 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 !
  • 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.