• 4 heures
  • Facile

Ce cours est visible gratuitement en ligne.

course.header.alt.is_certifying

J'ai tout compris !

Mis à jour le 30/03/2023

Rendez vos types génériques

Donnez des paramètres à vos types

Les génériques sont probablement l’un des concepts les plus puissants de TypeScript. Malheureusement, il n’est pas évident de bien comprendre leur intérêt lorsqu’on débute avec TypeScript. Je vous laisse regarder la vidéo ci-dessous et ensuite on reprend point par point les différentes étapes de la démonstration avec un exemple concret. C'est parti !

Plutôt que de partir dans une explication théorique un peu ennuyeuse, je vous propose un exemple dans lequel nous définirons le type de plusieurs sortes de boutiques : une armurerie vendant des pièces d’équipement, un apothicaire vendant des potions et une animalerie vendant des animaux de compagnie.

En simplifiant au mieux, voici les différentes propriétés qu’une boutique peut avoir :

  • son nom ;

  • son propriétaire ;

  • les objets à disposition.

Traduit en TypeScript, on pourrait donc écrire ce qui suit :

type Shop = {
    name: string;
    owner: Character; // Le même "Character" qu'on a vu au chapitre précédent
    items: Array<unknown>;
};

“unknown” ? Qu’est-ce que c’est ?

Vous vous souvenez quand j’ai dit que TypeScript proposait les types number  , boolean  et string  ? J’ai volontairement omis de vous parler d’autres types de base pour ne pas vous embrouiller. Ne vous sentez pas floué, s'il vous plaît. 😬

unknown  fait partie de ces types. Il s’agit du type “inconnu” : il est utilisé lorsqu’on ne sait pas exactement quel type on attend.

Sachez qu’il existe un type similaire, que je me dois d’évoquer ici : any  . La première chose à savoir sur cet autre type est très simple : je vous déconseille de l’utiliser !

Tout comme unknown  , le rôle de any  est de servir de joker : il est utile lorsqu’on ne sait pas typer quelque chose. Si on ne sait pas quel type d’argument va recevoir notre fonction, any  permet de dire à TypeScript qu’il doit s’attendre à recevoir n’importe quel type.

function sayHello(target: any) { return `Hello ${target.firstName}`; }
sayHello(123); // À cause de "any", TypeScript ne remonte aucune erreur ici !

Pourquoi parler de “any” s’il ne faut pas l’utiliser ?

Bref, retournons à notre exemple de boutiques, dans lequel nous avons donc écrit  items: Array<unknown>  . Cela signifie qu’une boutique peut vendre plusieurs objets, mais on ne sait pas de quel type… En soi, ce n’est pas faux, mais essayons d’être plus précis !

Comme on l’a vu auparavant, nous pouvons utiliser ce type Shop  avec une intersection pour définir une armurerie qui ne vend que des pièces d’équipement :

type Shop = {
    name: string;
    owner: Character; // Le même "Character" qu'on a vu au chapitre précédent
    items: Array<unknown>;
};
type Equipment = {
    price: number;
    attack?: number;
    defense?: number;
};
type Armory = Shop & {
    items: Array<Equipment>;
};

Pour l’apothicaire et l’animalerie, nous procéderons ainsi de la même manière :

type PetShop = Shop & {
    items: Array<Pet>; // Le même "Pet" qu'on a vu au chapitre précédent
};
type Apothecary = Shop & {
    items: Array<Potion>; // On admet qu'on a défini le type "Potion"
};

Entre ces trois exemples de boutiques, on remarque une certaine répétition dans le code : nous sommes obligés d’écrire à chaque fois l’intersection entre Shop  et un autre objet contenant une propriété items  au type souhaité.
Cette répétition peut être évitée en faisant de Shop  un générique.

Pour transformer un type en générique, il suffit d’ajouter après son nom les caractères <  et >  . Entre ces deux signes, on définit un nom de type, tout comme on définirait le nom d’un paramètre de fonction :

type Shop<ItemType> = { /* … */  };

Tout comme un paramètre de fonction, nous pouvons nommer les paramètres de génériques comme nous le souhaitons. Il est très courant de voir des exemples où ils sont simplement nommés T  , ou autre lettre unique. Ce n’est qu’un avis personnel, mais je ne trouve pas ça clair, surtout lorsqu’on débute dans le langage : je vous conseille donc de nommer ces paramètres le plus précisément possible !

Dans notre exemple, nous avons donc choisi de nommer ce “type-paramètre” ItemType  . Il servira à définir les types des objets vendus dans la boutique.
Le paramètre d’un générique est accessible et utilisable dans toute la définition du type Shop  , nous pouvons donc l’utiliser pour définir items  :

type Shop<ItemType> = {
    name: string;
    owner: Character;
    // Nous utilisons notre paramètre ici, à la place de "unknown"
    items: Array<ItemType>;
};

Le type Shop  étant un générique, la façon de l’utiliser doit désormais varier un petit peu : il est maintenant nécessaire de renseigner la valeur que doit avoir le type ItemType  (là encore, pensez aux arguments des fonctions !).

type Armory = Shop<Equipment>;
type PetShop = Shop<Pet>;
type Apothecary = Shop<Potion>;

Cela dit, comprenez bien que Shop<Equipment>  est un type à part entière. Lorsqu’on écrit  type Armory = Shop<Equipment>;   , nous écrivons bien un alias nommé Armory  de ce type Shop<Equipment>  (rappelez-vous le véritable rôle du mot-clé type  !).

Je préfère utiliser le mot-clé interface  plutôt que type  : puis-je aussi utiliser le concept des génériques ?

Bien entendu ! En fait, presque tout ce qui permet de définir un type TypeScript peut être générique : les alias donc, mais aussi les interfaces et même les fonctions !

// Équivalent du type générique que nous venons de voir, avec une interface
interface Shop<ItemType> {
    name: string;
    owner: Character;
    items: Array<ItemType>;
};
// Une fonction générique
function createShop<ItemType>(name: string, owner: Character, items: Array<ItemType>;): Shop<ItemType> {
    return { name, owner, items };
}
// Appel de la fonction générique
const armory = createShop<Equipment>('My armory', { name: 'Bob', life: 100, attack: 1, defense: 2 }, []);

Petit bonus pour les fonctions génériques : il n’est pas nécessaire de préciser le “paramètre type” si celui-ci est utilisé pour définir le type d’un des paramètres de la fonction. TypeScript est en effet capable de le deviner par lui-même !

// Une fonction qui retourne simplement ce qu'elle reçoit en paramètre
function returnParameter<T>(x: T): T {
    return x;
}
// Ceci fonctionne, c'est ce que nous avons vu jusque-là
const a = returnParameter<number>(1);
// Mais puisque le type "T" est utilisé pour typer le paramètre "x",
// il n'est pas nécessaire de le préciser en appelant la fonction.
// Avec la ligne ci-dessous, TypeScript devine tout seul que "T" est "number" !
const a2 = returnParameter(1);

Les génériques sont un excellent moyen de rendre vos types plus… génériques. 🙃

Les génériques permettent en effet à vos types d’être plus facilement réutilisables… mais au coût non négligeable de rendre votre code plus complexe.

Et si vous êtes un peu perdu devant ces nouveaux concepts : pas d’inquiétude ! Vous venez de découvrir les bases de ce qui a de plus complexe dans TypeScript, c’est donc tout à fait normal.

Utilisez les génériques proposés par TypeScript

Les génériques sont tellement intéressants que TypeScript propose toute une liste de génériques “natifs”. Et vous connaissez déjà le premier que nous allons voir ! En effet, Array  est un générique. Si nous devions le définir nous-même, voici comment on pourrait l’écrire :

type Array<T> = T[];

En plus de Array<T>  , TypeScript propose d’autres génériques pour nous aider à faire des opérations courantes.
On appelle ces génériques des utility types ou, en bon français, des “types utilitaires”. Voici deux exemples :

Partial<T>

Ce type utilitaire prend en paramètre un type représentant un objet, et il retourne un type représentant ce même objet, mais avec toutes ces propriétés marquées comme étant optionnelles.

type Character = {
    // Toutes les propriétés sont requises (elles n'ont pas le signe "?")
    name: string;
    life: number;
    attack: number;
    defense: number;
};
const myCharacter: Partial<Character> = {
    // On ne fournit que le nom, pas le reste des propriétés.
    // On n'a pas d'erreur car "Partial" rend
    // toutes les propriétés optionnelles.
    name: 'Mario',
};

Record<KeyType, ValueType>

Cet utilitaire permet de définir des types d’objets. Jusque-là, nous avons utilisé la notation avec les accolades {}  , mais il peut être plus adapté d’utiliser le générique  Record<KeyType, ValueType>   , comme dans les exemples qui suivent :

// On définit un type représentant un objet dont les clés
// sont des chaînes de caractères (n'importe lesquelles)
// et les valeurs sont des nombres
type CollectionOfNumbers = Record<string, number>;
const stats: CollectionOfNumbers = {
    age: 45,
    life: 100,
    magic: 10,
    whateverTheNameItMustContainANumber: 20,
};

// On peut utiliser une union pour n'autoriser que des clés spécifiques
type StatisticNames = 'life' | 'attack' | 'defense';
const stats: Record<StatisticNames, number> = {
    life: 100,
    attack: 10,
    defense: 20,
};

Lisez la documentation !

À vous de jouer !

Dans cet exercice, vous allez utiliser des génériques pour améliorer un code déjà existant. Le but est d’éviter des répétitions de définitions de propriétés en suivant trois consignes précises.

En résumé

  • Lorsqu’on souhaite typer une variable dont on ignore le type, TypeScript propose les types unknown  et  any  : ce dernier est d’ailleurs à éviter.

  • Les génériques sont un moyen d’ajouter des paramètres à vos types pour les rendre plus facilement réutilisables.

  • TypeScript propose toute une liste de génériques déjà existants, appelés les utility types.

Maintenant que vous en savez plus sur les génériques, vous avez désormais les bases pour vous attaquer sereinement à un projet TypeScript ! Il vous reste encore à découvrir des notions plus complexes à appréhender, mais vous pouvez d'ores et déjà profiter de la puissance de ce langage.
Dans le prochain chapitre, nous verrons d’ailleurs comment en profiter, même en utilisant des librairies écrites en JavaScript !

Exemple de certificat de réussite
Exemple de certificat de réussite