La généricité
Introduction
La généricité permet d’écrire du code fonctionnant sur plusieurs types plutôt qu’un seul. Son objectif est d’améliorer la réutilisabilité, ce qui permet d’éviter de dupliquer un bloc de code pour chaque type pouvant être utilisé par celui-ci. C’est un concept qui existe en programmation orientée objet depuis longtemps et qui a été implémenté dans TypeScript depuis la première version du langage.
Déclaration de base
La déclaration d’un type générique se fait par convention avec la lettre T. Il est possible de changer cette lettre, mais elle doit obligatoirement être encapsulée dans des chevrons.
Syntaxe :
<T>
Le tableau est un des exemples les plus simples permettant d’illustrer l’utilisation des génériques. Le fichier de déclaration de base de TypeScript (lib.d.ts) dispose d’une interface permettant de typer les éléments contenus dans un tableau. Cette interface utilise la généricité : Array<T>.
Exemple :
const salaries: Array<number> = [1700, 2250, 2000, 1850];
salaries.push(2125);
La déclaration d’un type générique se fait toujours après le nom de l’élément qui le porte. Une fois défini, le type générique peut être utilisé pour typer ce qui est contenu dans le bloc de code de l’élément portant la généricité (par exemple, pour une fonction, le type générique pourra être appliqué à : ses paramètres, son retour, ses variables…).
Exemple :
function add<T>(value: T) {
// Do something here
}
Lors de l’utilisation d’un élément définissant un générique, il est nécessaire...
Classes et interfaces
Les génériques peuvent être utilisés avec les classes et les interfaces. Une fois un générique défini sur une interface, il peut ensuite être utilisé pour typer :
-
une propriété,
-
les paramètres de méthode,
-
les retours de méthode.
Exemple :
interface Entity<T> {
readonly id: T;
setId(id: T): void;
getId(): T;
}
Lors de la déclaration d’une variable avec une interface définissant un générique, celui-ci doit être précisé.
Exemple :
interface Entity<T> {
readonly id: T;
}
const entityWithNumberId: Entity<number> = {
id: 1
};
// Log: 1
console.log(entityWithNumberId.id);
const entityWithStringId: Entity<string> = {
id: "2a826410-de77-4938-b640-571cd943f3f5"
};
// Log: "2a826410-de77-4938-b640-571cd943f3f5"
console.log(entityWithStringId.id);
// Compilation Error TS2345: Argument of type 'true' is
// not assignable to parameter of type 'string'.
const entityWithWrongType: Entity<string> = {
id: true
};
Un type générique défini sur une classe peut...
Contraintes
Une contrainte de type peut être précisée lors de la déclaration d’un type générique. Pour cela, il faut utiliser le mot-clé extends et définir ensuite la contrainte de type qui doit être respectée.
Syntaxe :
<T extends Type>
Lors de l’utilisation d’un élément définissant un générique avec une contrainte, il sera alors nécessaire d’utiliser un type conforme à celle-ci.
Exemple :
interface Payable {
salary: number
}
class Company {
sendPayments<T extends Payable>(toBePaid: T[]) {
toBePaid.forEach(p => {
console.log(`Pay salary: ${ p.salary }€`);
});
}
}
N’importe quelle forme de type peut être appliquée sur une contrainte. Il est donc possible d’utiliser :
-
les types primitifs (number, string, boolean…),
-
les interfaces,
-
les classes,
-
les alias de type (cf. chapitre Système de types avancés),
-
les singleton types (cf. chapitre Système de types avancés).
Plusieurs contraintes peuvent être appliquées à un même type générique en utilisant l’opérateur d’intersection & (cf. chapitre Système de types avancés).
Exemple :
interface Payable {
salary: number;
}
interface Person {
firstName: string;
lastName: string;
}
class Company {
sendPayments<T extends Payable & Person>(employees: T[]) {
employees.forEach(e => {
console.log(
`Pay ${e.firstName} ${e.lastName} | Salary: ${e.salary}€ ...
Générique et constructeur
Lors de la définition d’un type générique sur un élément, il n’est pas possible par la suite d’en créer des instances. Cela limite fortement certains scénarios courants en programmation orientée objet. Plusieurs langages permettent de définir la présence d’un constructeur au niveau d’une contrainte. Cela n’est pas possible actuellement en TypeScript.
Pour contrebalancer cette limitation, il existe une syntaxe particulière permettant de définir la signature d’un constructeur. Ce constructeur peut être utilisé pour renvoyer des instances de type générique.
Syntaxe :
ctor: new () => T
Cette syntaxe permet donc de passer le constructeur en paramètre (ou de l’assigner dans une variable) et de l’utiliser ensuite avec le mot-clé new pour créer des instances.
Exemple :
function create<T>(ctor: new () => T) {
return new ctor();
}
const emptyString = create(String);
// Log: true
console.log(emptyString instanceof String);
Cette syntaxe permet aussi de définir plusieurs paramètres attendus pour un constructeur.
Exemple :
class Person {
constructor(
public readonly firstName: string,
public readonly lastName: string
...