TypeScript : Typage et Généricité
La généricité permet d’écrire des définitions (de classes, interfaces, fonctions, types…) paramétriques. On appelle ces définitions des Génériques. Présents sous le même nom dans Java (1.5), C# (2), ils existent dans TypeScript depuis sa création (merci qui ? Merci Anders !).
Un exemple très commun en TypeScript est celui des tableaux, avec Array<T>
. T
est appelé “paramètres de type” (type parameter, à ne pas confondre avec un type de paramètre 🙃).
const items: Array<number> = [1, 2, 3];
items.push(4); // ok
items.push('a'); // erreur: argument of type 'a' is not assignable to parameter of type 'number'
// "n" est déduit comme étant de type 'number'
const results = items.map(n => n.toFixed(1)); // ["1.0", "2.0", "3.0", "4.0"]
Les mêmes méthodes (push
, map
, etc) et la même analyse statique existent avec Array<string>
, Array<Date>
, etc. Array
est dit “type générique”.
💡 C’est aussi vrai pour
Map<K, V>
,Set<T>
,Promise<T>
,Observable<T>
(RxJS), etc.
Show time 🎉
Certains exemples sont volontairement simplistes, l’objectif étant de lever toute ambiguïté en se concentrant sur les sujets de la généricité et du typage 🤗
Grâce aux generics, il est possible de :
🌟 Déclarer des contraintes génériques
On définit une fonction générique pick
type-safe permettant de récupérer un extrait d’objet, ici, à l’aide de K extends keyof T
, dit ”K
contraint par T
“.
function pick<T, K extends keyof T>(source: T, ...keys: K[]): Partial<T> {
const result: Partial<T> = {};
keys.forEach(key => result[key] = source[key]);
return result;
}
const user = { weight: 55, name: 'Winry', birthDate: new Date('1985-06-13') };
console.log(pick(user, 'name', 'weight')); // { name: 'Winry', weight: 55 }
Bien sûr, tout est analysé/déduit par le compilateur tsc
et l’IDE sait aussi faire l’auto-complétion des propriétés saisies (name
et weight
) 🔥.
🌟 Obtenir les propriétés d’un certain type d’une classe ou interface
En utilisant les types conditionnels (conditional types), qui prennent la forme d’une expression ternaire utilisant extends
:
type StringProperty<T> = { [P in keyof T]: T[P] extends string ? P : never }[keyof T];
class User {
birthDate: Date;
isAdmin: boolean
firstName: string;
lastName: string;
}
type S1 = StringProperty<User> // 'firstName' | 'lastName'
On peut rendre ce type générique … encore plus générique !
// toutes les propriétés de type "A" du type "T"
type PropertyOfType<T, A> = { [P in keyof T]: T[P] extends A ? P : never }[keyof T];
// décliné en :
type StringProperty<T> = PropertyOfType<T, string>;
type NumberProperty<T> = PropertyOfType<T, number>;
type MethodProperty<T> = PropertyOfType<T, (...args: any[]) => any>;
// ...
🌟 Définir des types élaborés (mapped types)
Rendus possibles grâce à la généricité, vous connaissez la plupart d’entre eux si vous utilisez TypeScript régulièrement :
Partial<T>
: Type ayant toutes les propriétés deT
, optionnelles (modificateur?
).Required<T>
: Type ayant toutes les propriétés deT
, requises (modificateur-?
).Readonly<T>
: Type ayant toutes les propriétés deT
, non ré-assignables (modificateurreadonly
).Pick<T, K extends keyof T>
: Type ayant toutes les propriétésK
deT
.
Quelques autres qui, bien qu’absents des lib.*.d.ts
, sont très communément utilisés dans les projets TypeScript:
// Type ayant toutes les propriétés de T, pouvant aussi être `null`
type Nullable<T> = { [P in keyof T]: T[P] | null };
// Type ayant toutes les propriétés de T, sans modificateur `readonly`
type Writable<T> = { -readonly [P in keyof T]: T[P] };
// Type ayant toutes les propriétés de T sauf celles données pour K
// e.g.: Omit<User, 'firstName' | 'lastName'>
type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
// `Partial<T>` supportant plusieurs niveaux de profondeur (sous-objets, sous-sous-objets, etc)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends Array<infer U>
? Array<DeepPartial<U>>
: DeepPartial<T[P]>
};
💡 L’implémentation de
DeepPartial
ci-dessus est incomplète (pour des raisons de lisibilité) : elle doit aussi intégrer l’assignation des typesDate
(à conserver “tel quel”) etReadonlyArray<T>
.
🌟 Introduire des variables de Type à déduire (infer
)
L’introduction des types conditionnels (TypeScript 2.8), avec le mot-clé extends
, a aussi introduit le mot-clé infer
, qui permet de déclarer une variable de type déduit.
DeepPartial<T>
en montre un premier exemple ci-dessus, en transformant les Array<U>
en Array<DeepPartial<U>>
(souhaité) et non en DeepPartial<Array<U>>
(insensé).
Autres exemples :
// Type de retour d'une fonction T, introduit comme "R"
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
// Type déduit "U" d'une Promise, d'un Array, d'une fonction, ou T
type Unpacked<T> =
T extends (infer U)[] ? U :
T extends (...args: any[]) => infer U ? U :
T extends Promise<infer U> ? U :
T;
type U1 = Unpacked<number[]>; // number
type U2 = Unpacked<Promise<string>>; // string
type U3 = Unpacked<Unpacked<Promise<string[]>>>; // string
type U4 = Unpacked<Date>; // Date
// Type (tuple) contenant les types des paramètres d'une fonction
type Parameters<T extends (...args: any[]) => any> = T extends (...args: infer P) => any ? P : never;
// type T1 = Parameters<typeof Math.min> // [number[]]
// Type (tuple) contenant les types des paramètres d'un constructeur
type ConstructorParameters<T extends new (...args: any[]) => any> = T extends new (...args: infer P) => any ? P : never;
// type T2 = ConstructorParameters<typeof Account>
🌟 Utiliser un “paramètre du reste” générique
Dans cet exemple, avec ...T[]
.
On définit un type ColorFilter
attendant soit : une et une seule valeur de type Color
, ou un opérateur 'AND'
ou 'OR'
suivi d’au moins deux valeurs de type Color
.
type FilterAndOr<T, K = 'AND' | 'OR'> = [T] | [K, T, T, ...T[]];
type Color = 'red' | 'green' | 'blue' | 'yellow' | 'white';
// Permet l'utilisation des valeurs:
// ["blue"]
// ["AND", "blue", "red"]
// ["OR", "yellow", "green", "red"]
// ["AND", "blue", "white", "red", "green"]
type ColorFilter = FilterAndOr<Color>;
🌟 Etc, etc, etc
Il existe une infinité de cas d’usage des Generics et il m’est impossible d’en faire une liste exhaustive, cependant, comme tout bon outil, attention à ne pas tomber dans le piège habituel : quand on a un marteau, tout ressemble à un clou 😁.
Pour toujours plus de TypeScript 💕, je vous invite à regarder cette vidéo d’Anders Hejlsberg à la DotJS 2018 à Paris. Enjoy!
Retrouvez-moi sur Twitter :
@VinceOPS