Génériques
Si vous considérez les types comme des variables, alors les génériques pourraient être considérés comme des fonctions. Ils vous permettent de créer des types qui sont paramétrés par d'autres types.
Les génériques sont une fonctionnalité puissante de TypeScript qui vous permet de créer des types réutilisables qui peuvent fonctionner dans divers contextes.
Génériques dans les signatures de fonction
Un cas d'usage courant pour les génériques est de créer des fonctions et d'indiquer une relation entre les arguments et le type de retour.
Prenons un exemple simple d'une fonction qui retourne le premier élément d'un tableau :
function firstElement<T>(arr: T[]): T | undefined {
return arr[0];
}
Pour déclarer un type générique, vous utilisez la syntaxe des crochets angulaires <T>
. Ici T
est une variable de type qui peut être utilisée dans la signature de fonction. Elle est utilisée dans l'annotation de type d'argument pour arr
comme T[]
, signifiant que la fonction attend un tableau d'un type générique, puis elle est utilisée dans la définition du type de retour comme T | undefined
, signifiant que la fonction retournera soit un élément de ce type générique T
soit undefined
si le tableau est vide.
N'importe quel nombre de types génériques peut être utilisé. Par exemple, une fonction qui retourne aléatoirement soit le premier argument soit le second :
function randomElement<A, B>(a: A, b: B): A | B {
return Math.random() < 0.5 ? a : b;
}
Par convention, les types génériques sont le plus souvent nommés avec une seule lettre majuscule, mais vous pouvez utiliser n'importe quel nom que vous voulez.
Génériques dans les définitions de type
Les génériques peuvent aussi être utilisés dans les définitions de type. Par exemple, un type qui représente une paire de valeurs de types différents :
type Pair<First, Second> = [First, Second];
ou un type de collection générique :
type Stack<T> = {
length: number;
push: (item: T) => void;
pop: () => T | undefined;
};
Dans ce cas, vous devrez spécifier le type T
quand vous utilisez le type Stack
:
const logs: Stack<string> = ["hello world"];
Contraintes de génériques avec extends
Par défaut, un type générique peut prendre n'importe quel type comme paramètre. Cependant, vous pouvez restreindre les types qui peuvent être utilisés avec un type générique en utilisant le mot-clé extends
.
Nous avons déjà vu ce mot-clé extends
dans le chapitre précédent avec les types conditionnels. U extends T
est utilisé pour indiquer que le type U
est soit T
, soit un sous-type de T
, c'est-à-dire que toute valeur de type U
est également de type T
, mais toutes les valeurs de type T
ne sont pas forcément de type U
. Par exemple, string extends string
est valide mais aussi string extends string | number
, ou "hello" extends string
.
type T = "hello" extends string ? true : false; // T = true
Dans le cadre des génériques, extends
donnera plus de contrôle au développeur et lèvera des erreurs si le type ne correspond pas à la contrainte. Voici quelques exemples :
type HasLength = { length: number };
function longest<A extends HasLength, B extends HasLength>(a: A, b: B): A | B {
return a.length >= b.length ? a : b;
}
longest("string", ["a", "r", "r", "a", "y"]);
longest([1, 2, 3], new Set([4, 5, 6]));
// Erreur : Property 'length' is missing in type 'Set<number>'
// but required in type 'HasLength'
function getProperty<Obj, Key extends keyof Obj>(obj: Obj, key: Key): Obj[key] {
return obj[key];
}
let x = { a: 1, b: 2, c: 3, d: 4 };
getProperty(x, "a");
getProperty(x, "m");
// Erreur : Argument of type '"m"' is not assignable to parameter
// of type '"a" | "b" | "c" | "d"'.
Exercice 1
Suivez les instructions dans l'éditeur en ligne TypeScript pour un exercice pratique sur les génériques.
Valeurs par défaut des types génériques
Un type générique peut avoir une valeur par défaut, qui sera utilisée si le type en argument n'est pas spécifié lors de l'utilisation du type générique. Exemple :
type Stack<T = string> = {
length: number;
push: (item: T) => void;
pop: () => T | undefined;
};
const stack1: Stack = [];
stack1.push("hello world");
stack1.push(42); // Erreur : Argument of type '42' is not assignable
// to parameter of type 'string'
function createStack<T = string>(initial: T[] = []): Stack<T>;
const stack = createStack(); // stack est de type Stack<string> et est vide
const numberStack = createStack<number>([1, 2]); // Stack<number> contenant [1, 2]
Le mot-clé infer
Le mot-clé infer
est utilisé dans les types conditionnels pour inférer un type à partir d'un autre type. Il est aussi utilisé dans les génériques pour inférer un type à partir d'une valeur. Par exemple, voici comment vous pouvez inférer le type d'un élément de tableau :
type ElementType<A> = A extends (infer E)[] ? E : never;
type T = ElementType<string[]>; // T = string
type U = ElementType<number[]>; // U = number
Voici comment vous pouvez inférer le type de retour d'une fonction :
type ReturnType<F> = F extends (...args: any[]) => infer R ? R : never;
type T = ReturnType<typeof Math.max>; // T = number
Ou le type du premier argument d'une fonction :
type FirstArg<F> = F extends (first: infer A, ...args: any[]) => any ? A : never;
type T = FirstArg<typeof String.fromCharCode>; // T === number
Dans cet exemple, FirstArgument
est un type générique qui prend un type de fonction F
comme paramètre. Il vérifie si F
est une fonction qui prend au moins un argument, et si c'est le cas, le mot-clé infer
est utilisé pour extraire le type du premier argument du type de fonction.
Notez les mots-clés never
dans la seconde partie du conditionnel. C'est un type spécial qui ne correspond à aucune valeur du tout. C'est essentiellement un ensemble vide utilisé pour lever une erreur si l'argument passé à FirstArgument
n'est pas une fonction avec au moins un argument.
Exercice 2
Suivez les instructions dans l'éditeur TypeScript en ligne
ANECDOTE
Les génériques permettent à TypeScript d'être un système de types Turing-complet, ce qui signifie qu'il peut être utilisé pour calculer n'importe quoi de calculable. Cela a été brillamment démontré récemment par quelqu'un qui a réussi à faire tourner le jeu vidéo Doom dans le système de types TypeScript.