Introduction : Que sont les Generics et pourquoi sont-ils importants ?
\n\nLors de l'apprentissage de TypeScript, vous rencontrez rapidement des situations où vous devez écrire une fonction ou une classe qui peut fonctionner avec différents types de données. Sans outils spéciaux, vous devriez soit dupliquer le code pour chaque type, soit utiliser any, ce qui tue toute la magie du typage strict. C'est là que les Generics (génériques ou généralisations) viennent à la rescousse.
Generics est un mécanisme puissant de TypeScript qui permet de créer des composants (fonctions, classes, interfaces) fonctionnant avec différents types, tout en conservant un typage strict. Imaginez que vous écrivez un conteneur universel qui peut stocker à la fois des pommes, des livres et des nombres, mais TypeScript sait exactement ce qui se trouve à l'intérieur dans chaque cas spécifique.
\n\nDans cet article, nous allons voir pourquoi les Generics sont nécessaires, comment les déclarer et les utiliser dans des projets réels. Vous apprendrez à rendre votre code plus flexible, plus sûr et plus réutilisable.
\n\nBases des Generics : syntaxe et première fonction
\n\nCommençons par un exemple simple. Supposons que nous ayons besoin d'une fonction qui prend un argument et le retourne (fonction identité). Sans génériques, nous écririons ceci :
\n\nfunction identity(arg: number): number {\n return arg;\n}\n\nMais cette fonction ne fonctionne qu'avec les nombres. Pour la rendre universelle, nous pouvons utiliser any :
function identity(arg: any): any {\n return arg;\n}\n\nLe problème est que nous perdons l'information sur le type. TypeScript ne sait pas que le type retourné sera exactement le même que celui passé. C'est là que les Generics interviennent :
\n\nfunction identity<T>(arg: T): T {\n return arg;\n}\n\n// Utilisation :\nlet output1 = identity<string>("Bonjour"); // type string\nlet output2 = identity<number>(42); // type number\n\nAnalysons la syntaxe :
\n- \n
<T>— c'est le paramètre de type. Vous pouvez utiliser n'importe quelle lettre, mais par convention on utilise souventT(Type),U,V. \n arg: T— l'argument est de typeT. \n : Taprès les parenthèses — la fonction retourne une valeur de typeT. \n
Maintenant, TypeScript sait : si vous avez passé une chaîne, le résultat sera aussi une chaîne. Si un nombre, ce sera un nombre. L'inférence de type fonctionne souvent automatiquement, donc vous pouvez simplement écrire identity("Bonjour") — TypeScript comprendra que T est string.
Generics dans les fonctions : travail avec les tableaux et contraintes
\n\nLe plus souvent, les génériques sont utilisés pour travailler avec des tableaux ou des collections. Considérons une fonction qui retourne le premier élément d'un tableau :
\n\nfunction getFirst<T>(arr: T[]): T | undefined {\n return arr[0];\n}\n\nconst numbers = [1, 2, 3];\nconst firstNumber = getFirst(numbers); // type number\n\nconst strings = ["a", "b", "c"];\nconst firstString = getFirst(strings); // type string\n\nNotez : T[] est une notation abrégée pour Array<T>. La fonction fonctionne avec n'importe quel tableau, mais retourne un élément du type exact stocké dans le tableau.
Contraintes avec extends
\n\nParfois, vous avez besoin que le type T ait certaines propriétés. Par exemple, nous voulons obtenir la longueur d'un objet. Tous les types n'ont pas la propriété length, nous pouvons donc imposer une contrainte :
interface HasLength {\n length: number;\n}\n\nfunction getLength<T extends HasLength>(arg: T): number {\n return arg.length;\n}\n\ngetLength("chaîne"); // OK, une chaîne a length\ngetLength([1, 2, 3]); // OK, un tableau a length\ngetLength(42); // Erreur ! Un nombre n'a pas length