Les génériques dans TypeScript ne sont pas faciles à comprendre quand on débute avec TS. Personnellement, j'ai eu des difficultés avec eux au début, cependant, une fois que vous comprenez comment ils sont utilisés, ils font de vous un développeur typescript freelance plus complet.

Dans ce tutoriel TypeScript, vous allez apprendre comment utiliser les génériques en TypeScript. Nous commencerons par définir une expression JavaScript de type flèche (aussi appelée fonction flèche) qui prend un objet (ici : dog) et retourne sa propriété age :

const getAge = (dog) => {
  return dog.age;
};

Ensuite, nous appellerons cette fonction avec un objet qui a cette propriété requise :

const trixi = {
  name: 'Trixi',
  age: 7,
};

console.log(getAge(trixi));
// 7

Maintenant, si nous voulions définir ce code en TypeScript, il serait modifié de la manière suivante :

type Dog = {
  name: string;
  age: number;
};

const getAge = (dog: Dog) => {
  return dog.age;
};

const trixi: Dog = {
  name: 'Trixi',
  age: 7,
};

console.log(getAge(trixi));
// 7

Cependant, la fonction est spécifique à un type TypeScript (ici : Dog) maintenant. Si nous utilisions une valeur d'un type différent comme argument (par exemple Person), il y aurait une erreur TypeScript, car les deux types diffèrent dans leur structure :

type Person = {
  firstName: string;
  lastName: string;
  age: number;
};

const johan: Person = {
  firstName: 'Johan',
  lastName: 'Petrikovsky',
  age: 7,
};

console.log(getAge(johan));
// Argument of type 'Person' is not assignable to parameter of type 'Dog'.
// Property 'name' is missing in type 'Person' but required in type 'Dog'.

La fonction fléchée attend un argument de type Dog, comme il est défini dans la signature de la fonction, mais dans l'exemple précédent, elle a reçu un argument de type Person qui a des propriétés différentes (même si les deux partagent la propriété age) :

const getAge = (dog: Dog) => {
  return dog.age;
};

En plus de donner au paramètre un nom plus abstrait mais descriptif, une solution serait d'utiliser un TypeScript union type :

const getAge = (mammal: Dog | Person) => {
  return mammal.age;
};

Et cette solution conviendrait à la plupart des projets TypeScript. Cependant, dès qu'un projet prend de l'ampleur (verticalement et horizontalement), vous aurez très certainement besoin des génériques TypeScript, car la fonction doit accepter n'importe quel type générique (vous pouvez aussi lire : abstrait) qui remplit toujours certaines conditions (ici : avoir une propriété age).

Entrons dans les types génériques TypeScript...

Les types génériques en TypeScript

Une fois qu'un projet grandit horizontalement en taille (par exemple, plus de domaines dans un projet), une fonction abstraite comme getAge peut recevoir plus de deux types (ici : Dog et Person) comme arguments. En conclusion, il faudrait également faire évoluer le type d'union horizontalement, ce qui est fastidieux (mais fonctionne toujours) et source d'erreurs.

type Mammal = Person | Dog | Cat | Horse;

Dans la direction orthogonale, lorsqu'un projet croît verticalement en taille, les fonctions qui deviennent plus réutilisables et donc abstraites (comme getAge) devraient plutôt traiter des types génériques au lieu de types spécifiques au domaine (par exemple Dog, Person).

Cas d'utilisation populaire : Le plus souvent, vous verrez ceci dans des bibliothèques tierces qui ne connaissent pas le domaine de votre projet (par exemple, chien, personne), mais qui ont besoin d'anticiper tout type qui remplit certaines conditions (par exemple, la propriété age requise). Dans ce cas, les bibliothèques tierces ne peuvent plus utiliser les types union comme échappatoire, car elles ne sont plus entre les mains du développeur qui travaille sur le projet réel.

En conclusion, si la fonction getAge doit gérer n'importe quelle entité avec une propriété age, elle doit être générique (lire : abstraite). Par conséquent, nous devons utiliser une sorte de placeholder pour utiliser un type générique qui est le plus souvent implémenté comme T :

type Mammal = {
  age: number;
};

const getAge = <T extends Mammal>(mammal: T) => {
  return mammal.age;
};

Alors que le T extends Mammal représente tout type qui a une propriété age.

type Person = {
  firstName: string;
  lastName: string;
  age: number;
};

const robin: Person = {
  firstName: 'Robin',
  lastName: 'Wieruch',
  age: 7,
};

console.log(getAge(johan));

Vous avez maintenant utilisé avec succès un générique TypeScript. La fonction abstraite getAge() prend comme argument tout objet qui a une propriété age. Négliger la propriété age nous donnerait une erreur TypeScript :

type Mammal = {
  age: number;
};

const getAge = <T extends Mammal>(mammal: T) => {
  return mammal.age;
};

type Person = {
  firstName: string;
  lastName: string;
  age?: number;
};

const robin: Person = {
  firstName: 'Johan',
  lastName: 'Petrikovsky',
  // age: 7,
};

console.log(getAge(johan));
// Argument of type 'Person' is not assignable to parameter of type 'Mammal'.
//   Types of property 'age' are incompatible.
//     Type 'number | undefined' is not assignable to type 'number'.
//       Type 'undefined' is not assignable to type 'number'

Les génériques sont largement utilisés dans les bibliothèques tierces. Si une bibliothèque tierce implémente correctement les génériques, vous n'avez pas besoin d'y penser lorsque vous utilisez ces bibliothèques abstraites dans votre application TypeScript spécifique au domaine.