Blog ENI : Toute la veille numérique !
🎁 Jusqu'au 25/12 : 1 commande de contenus en ligne
= 1 chance de gagner un cadeau*. Cliquez ici
🎁 Jusqu'au 31/12, recevez notre
offre d'abonnement à la Bibliothèque Numérique. Cliquez ici

Système de types avancés

Introduction

Ce chapitre explique des concepts avancés sur le système de types. Les fonctionnalités présentées dans ce chapitre permettront à vos programmes d’arriver à un niveau de typage élevé, ce qui diminuera fortement le risque d’erreurs lors de leur exécution.

Alias de type

Les alias de type permettent de définir des types réutilisables et/ou de renommer des types natifs.

Syntaxe :

type alias = type; 

Exemple :

type Salary = number; 
 
type Employee = { 
  firstName: string; 
  lastName: string; 
  salary: Salary; 
}; 
 
const increaseSalary = (percent: number, employee: Employee) => { 
  const increase = employee.salary * (percent / 100); 
  return employee.salary + increase; 
}; 
 
const evelyn: Employee = { 
  firstName: "Evelyn", 
  lastName: "Miller", 
  salary: 2000 
}; 
 
const newSalary = increaseSalary(2, evelyn); 
 
// Log: 2040 
console.log(newSalary); 

Les alias de type permettent de décrire des structures et possèdent beaucoup de points communs avec les interfaces. La plupart des fonctionnalités liées au typage d’une interface peuvent être utilisées avec un alias de type : propriété optionnelle, propriété en lecture seule, générique…

Exemple :

type Employee = { 
  firstname: string; 
  lastname: string; 
  readonly salary: number; 
}; 
 
type ListType<T> = Array<T>; 
 
let employees: ListType<Employee>...

Type never

Le type never représente un type qui pourrait être qualifié d’impossible et qui ne doit jamais arriver. Utilisé dans la signature d’une fonction, il indique que celle-ci ne peut pas retourner de résultat. Cela se produit dans le cas où la fonction lève toujours une erreur ou si elle contient une boucle infinie.

Exemple :

function throwError(message: string): never { 
  throw new Error(message); 
} 
 
function worker(): never { 
  while (true) {} 
} 

Contrairement au type void (cf. chapitre Types et instructions basiques), le type never n’est pas toujours correctement déduit par le compilateur de TypeScript. Il est parfois nécessaire de le préciser explicitement.

Le type never est également utilisé pour typer des variables. Une variable de type never n’accepte aucun type, mais tous les types l’acceptent. Cela signifie qu’il est impossible d’assigner une valeur à une variable de type never, mais qu’elle peut être assignée à une variable d’un autre type.

Exemple :

let never: never; 
let any: any; 
let unknown: unknown; 
 
// Compilation error TS2322: Type 'any' is 
// not assignable to type 'any'. 
const a: never = any; 
 
// Compilation error TS2322: Type 'any'...

Type union et intersection

1. Union

En TypeScript, il est possible d’exprimer des unions de types à l’aide de l’opérateur "|". Il est interprété comme un opérateur "OU" ("OR" en anglais) et permet d’indiquer qu’une variable peut être de typeA ou de typeB ou de typeN.

Syntaxe :

let/const variable: typeA | typeB | ... ; 

Le compilateur de TypeScript ne permet d’accéder qu’aux propriétés communes à tous les types précisés dans l’union.

Exemple (Visual Studio Code) :

images/08RI01.png

Toutefois, il est possible de restreindre une union à un type contenu dans celle-ci, on parle alors de discrimination.

Exemple :

interface Manager { 
  salary: number; 
} 
 
interface Salesman { 
  salary: number; 
  bonus: number; 
} 
 
function getSalary(person: Manager | Salesman) { 
  if ((person as Salesman).bonus) { 
    return person.salary + (person as Salesman).bonus; 
  } 
  return person.salary; 
} 

Dans cet exemple, le paramètre person est restreint au type Salesman grâce à une affirmation de type. Celle-ci permet d’accéder aux propriétés propres à l’interface Salesman afin d’appliquer un comportement spécifique....

Type guards

1. Introduction

Le compilateur ne donne accès qu’aux propriétés communes d’une union de types. Pour obtenir des informations plus précises sur la nature des types contenus dans l’union, il est nécessaire d’utiliser une discrimination. Pour réaliser celle-ci, il est impératif d’utiliser une condition (bloc if...else, ternaire ou bloc switch). Ces conditions discriminantes sont appelées les type guards (garde-type en français).

Pour rappel, les types définis en TypeScript ne sont utiles qu’à la compilation et n’existent pas dans le code JavaScript généré. Les type guards servent donc aussi à affirmer le type d’une variable lors de l’exécution d’un programme. Il existe plusieurs opérateurs qui permettent de réaliser un type guard natif : typeof, instanceof et in.

Il est également possible d’utiliser une propriété de type littéral (ou un modèle de type littéral - cf. section Modèle de type littéral) qui sera commune aux types composant l’union et utilisée en tant que propriété discriminante (cf. section Union discriminante).

Enfin, il existe une dernière possibilité qui permet de créer ses propres type guards à l’aide de fonctions appelées user defined type guard functions (fonctions garde-type définies par l’utilisateur). Dans ces fonctions, c’est à l’utilisateur de définir comment discriminer un type et de retourner un booléen de confirmation (cf. section Type guards définis par l’utilisateur).

2. Opérateur typeof

Cet opérateur unaire retourne sous forme de chaîne de caractères le type de l’opérande qui le suit. Ce type est celui déterminé par l’interpréteur JavaScript lors de l’exécution du programme (et non le type statique utilisé en TypeScript).

Exemple :

const personn = { 
  firstName: "Evelyn", 
  lastName: "Miller" 
}; 
 
const age = 34; 
 
// Log : object 
console.log(typeof person); 
 
// Log : string 
console.log(typeof person.firstName); 
 
// Log : number ...

Types littéraux

Les types littéraux permettent de restreindre les valeurs possibles pour un paramètre ou une variable. Il existe trois familles de types littéraux : les chaînes de caractères, les numériques et les booléens.

Exemple :

let firstName: "Evelyn" = "Evelyn"; 
 
// Compilation Error TS2322: Type 'Miller' is  
// not assignable to type 'Evelyn'. 
firstName = "Miller"; 
 
let age: 34 = 34; 
 
// Compilation Error TS2322: Type '35' is  
// not assignable to type '34'. 
age = 35; 
 
let isCeo: true = true; 
 
// Compilation Error TS2322: Type 'false' is  
// not assignable to type 'true'. 
isCeo = false; 

Cet exemple permet de mieux comprendre l’unicité intrinsèque d’un type littéral. En effet, les types littéraux représentent des sous-ensembles logiques d’autres types (number pour le type 34, string pour le type "Evelyn" et boolean pour le type false). Cette unicité n’exclut pas les caractéristiques s’appliquant au type de référence. Il est possible, par exemple, d’utiliser l’ensemble des méthodes string sur le type "Evelyn".

Exemple (Visual Studio Code) :

images/08RI13.png

Comme vu précédemment dans ce chapitre...

Modèle de type littéral

Les modèles de types littéraux (Template Literal Types en anglais) sont basés sur ceux vus précédemment, en combinaison de la syntaxe d’interpolation de chaînes de caractères. Ils permettent d’utiliser une ou plusieurs expressions de type et/ou alias de type défini(s) précédemment au sein d’un autre type.

Exemple :

type employeeName = "Evelyn";  
  
// type checkEmployee = "Check employee : Evelyn"  
type checkEmployee = `Check employee : ${employeeName}`; 

Ils peuvent être utilisé pour typer la signature d’une fonction.

Exemple :

function greetingEmployee(  
  firstname: string,   
  lastname: string  
): `Welcome ${string} ${string}` {  
  return `Welcome ${firstname} ${lastname}`;  
} 

Ils sont utilisables en combinaison avec d’autres fonctionnalités de TypeScript autour des types.

Exemple 1 (Union) :

type employeeName = "Evelyn" | "Patrick";  
  
// type checkEmployee = "Check employee : Evelyn" |   
//                      "Check employee : Patrick"  ...

Type index

Lorsqu’un type objet est défini en TypeScript, ses propriétés doivent être explicitement déclarées. Une propriété non déclarée dans le type ne peut pas être ajoutée dynamiquement par la suite.

Exemple :

interface Employee { 
  name: string; 
} 
 
// Compilation error TS2322:  
// Type '{ name: string; salary: number; }' 
// is not assignable to type 'Employee'. 
// Object literal may only specify known properties,  
// and 'salary' does not exist in type 'Employee'. 
const employee1: Employee = { 
  name: "Evelyn", 
  salary: 10000 
}; 

Le type index permet de typer l’index d’un objet.

Syntaxe :

interface InterfaceName { 
  [key: type] : type; 
} 
 
type TypeName = { 
  [key: type] : type; 
} 
 
class ClassName = { 
  [key: type] : type; 
} 

Ce type permet d’ajouter à un objet des propriétés arbitraires et de contraindre le type de celles-ci.

Exemple :

class Employee { 
  constructor(public readonly name: string) { } 
} 
 
class Manager { 
  constructor(public readonly title: string) { } 
} 
 
interface EmployeeList { 
  [key: string]: Employee; // Index Signature 
} 
 
const list: EmployeeList = {}; 
list["evelyn"] = new Employee("Evelyn"); 
list.evelyn = new Employee("Evelyn"); 
 
// Compilation error TS2741: 
// Property 'name'...

Mapped type

1. Avant TypeScript 2.1

Les mapped types (types mappés) sont des types qui prennent en paramètre un générique (cf. chapitre La généricité) prenant en paramètre un type objet. Ils permettent d’itérer sur les propriétés d’un type et de les modifier, créant ainsi un nouveau type. Pour bien comprendre leur intérêt, voici un premier exemple écrit en TypeScript 2.0, avant l’existence des mapped types. Cet exemple utilise la méthode Object.freeze définie dans le fichier de déclaration de base de TypeScript (lib.d.ts).

Exemple (TypeScript 2.0) :

freeze<T>(o: T): T; 

La méthode Object.freeze() permet d’empêcher toute modification sur un objet. Il n’est alors plus possible de modifier, ajouter ou supprimer les propriétés de cet objet.

Exemple (TypeScript 2.0) :

interface Employee { 
  name: string; 
  salary: number; 
} 
 
interface ReadonlyEmployee { 
  readonly name: string; 
  readonly salary: number; 
} 
 
function freezeEmployee(p: Employee): ReadonlyEmployee { 
  return Object.freeze(p); 
} 
 
const employee: Employee = { 
  name: "Evelyn", 
  salary: 10000 
}; 
 
const immutableEmployee1 = Object.freeze(employee); 
immutableEmployee1.name = "John"; 
 
const immutableEmployee2 = freezeEmployee(employee); 
 
// Compilation error TS2540: 
// Cannot assign to 'name' because 
// it is a read-only property. 
immutableEmployee2.name = "John"; 

Avant TypeScript 2.1, la méthode Object.freeze était définie par le fichier lib.d.ts pour retourner le type T de l’objet qui lui est passé en paramètre. Comme le montre l’exemple précédent, l’objet employeeImmutable1 n’est donc pas immutable. La modification de sa propriété name provoquera une erreur lors de l’exécution du programme, mais aucune erreur lors de la compilation.

Pour corriger cela, il fallait alors impérativement :

  • Ajouter une interface décrivant le même objet, mais ayant toutes ses propriétés en...

Affirmation constante

L’affirmation constante permet de rendre une variable immutable. Elle n’est utilisable qu’avec les variables de type primitif, les tableaux ou les objets littéraux. Elle n’a pas les mêmes effets selon qu’il s’agit d’une variable de type primitif ou d’un objet. Tout comme l’affirmation de type, elle possède deux notations.

Syntaxe 1 :

let/const variable = value as const; 

Syntaxe 2 :

let/const variable = <const> value; 

En TypeScript, on peut déclarer une variable à l’aide du mot-clé const ou let. Il existe une différence notable au niveau du typage.

Exemple (let - Visual Studio Code) :

images/08RI18.png

Exemple (const - Visual Studio Code) :

images/08RI19.png

Lors de l’utilisation du mot-clé const, le type devient littéral, ce qui empêche de le réassigner. Il est possible d’arriver au même résultat avec le mot-clé let afin de rendre la variable non modifiable via une affirmation constante.

Exemple (Visual Studio Code) :

images/08RI20.png

Dans le cas des objets, l’affirmation constante va appliquer le mot-clé readonly sur l’ensemble des propriétés de l’objet.

Exemple :

let employee = { 
  name: "Evelyn", 
  salary: 10000, 
  supplies: ["laptop", "bag"] 
} as const; 
 
// Compilation error TS2540: ...

Tuples variadiques

Les tuples ont déjà été présentés dans le chapitre Types et instructions basiques.  Il existe une particularité, présente depuis TypeScript 4.0, qui n’a pas encore été abordée : les tuples variadiques. Avant l’apparition de cette fonctionnalité, l’utilisation de l’opérateur rest (cf. chapitre Types et instructions basiques) sur des tuples ne permettait pas un typage efficace.

Exemple :

function concat(tuple1, tuple2) {  
  return [...tuple1, ...tuple2];  
} 

Dans l’exemple ci-dessus, une fonction permettant de concaténer deux tuples est définie. Sans les tuples variadiques, la seule façon de typer cette fonction est l’utilisation de surcharges.

Exemple :

function concat(tuple1: [], tuple2: []): [];  
function concat<A>(tuple1: [A], tuple2: []): [A];  
function concat<A, B>(tuple1: [A, B], tuple2: []): [A, B];  
function concat<A, B, C>(tuple1: [A, B, C], tuple2: []): [A, B, C]; 
function concat<A2>(tuple1: [], tuple2: [A2]): [A2];  
function concat<A1, A2>(tuple1: [A1], tuple2: [A2]): [A1, A2];  
function concat<A1, B1, A2>(tuple1: [A1, B1], tuple2: [A2]): [A1, B1, A2]; 
//... 

Chaque surcharge va typer un scénario possible. La première surcharge prend...

Type conditionnel

1. Les bases

Les types conditionnels permettent d’aller plus loin que les mapped types dans la manipulation des types. Ce sont également des types génériques prenant un type T en paramètre. La particularité des types conditionnels est qu’ils utilisent une condition au sein de leur déclaration. La condition va tester la compatibilité entre le type T et un type choisi, puis le type conditionnel retournera un nouveau type en fonction du résultat.

Syntaxe :

T extends U ? A : B 

Exemple (Visual Studio Code) :

images/08RI22.png

Exemple (Visual Studio Code) :

images/08RI23.png

Dans cet exemple, l’expression T extends Employee signifie que le type T est assignable au type Employee. L’opérateur ternaire peut donc être traduit de la façon suivante : si le type T est un sous-ensemble d’Employee, alors le type "yes" est retourné, sinon "no" est retourné.

Exemple (Visual Studio Code) :

images/08RI24.png

Dans ce nouvel exemple, Salesman est assignable à Employee car il contient les propriétés name et salary. La condition renvoie donc le type "yes".

Un type conditionnel peut se référencer lui-même et donc être récursif. Cette technique est à utiliser avec parcimonie car elle peut rapidement devenir coûteuse en temps de compilation. Elle a été introduite avec TypeScript 4.1 notamment pour permettre l’expression de certains types en profondeur coûteuse, comme le type utilitaire Awaited défini dans le fichier lib.d.ts.

2. Distributivité

Un type T passé en argument d’un type conditionnel peut être une union. Si ce type T est utilisé dans l’opérande de gauche du mot-clé extends, alors la condition est distribuée sur l’ensemble des types de l’union.

Exemple :

interface Employee { 
  name: string; 
  salary: number; 
} 
 
interface Manager { 
  name: string; 
  salary: number; 
  team: Employee[]; 
} 
 
interface CEO { 
  name: string; 
  salary: number; 
  team: Manager[]; 
} 
 
type Person = Employee | Manager | CEO; ...

Opérateur satisfies

Il arrive parfois que l’on souhaite conserver l’inférence du compilateur lors de l’utilisation d’un objet littéral et de vérifier qu’il est conforme à un type. Lorsque ce type contient une union, le compilateur TypeScript va prendre en compte par la suite uniquement les éléments communs à l’union (cf. section Type union et intersection - Union). Le compilateur utilisera alors le type explicite plutôt que l’inférence pour valider le code.

Dans un tel cas, il n’est plus possible d’utiliser un élément qui aurait pu être déterminé via l’inférence de type.

Exemple :

interface Manager { 
  salary: number; 
} 
 
interface Salesman {  
  salary: number; 
  bonus: number;  
} 
 
type Employee = Manager | Salesman;  
 
type Team = { 
  employees: Employee[], 
}; 
 
function getSalary(salary: number, bonus?: number) {  
  return bonus ? salary + bonus : salary;  
} 
 
const team: Team = { 
  employees: [{  
    salary: 2000, 
    bonus: 10 
  }, { 
    salary: 2000, 
  }] ...