Exploration du système de types
Le service de langage TypeScript
Le service de langage TypeScript, également appelé tsserver, offre diverses fonctionnalités telles que le signalement des erreurs, les diagnostics, la compilation lors de l’enregistrement, le renommage, l’accès à la définition, les listes de complétion, l’aide sur les signatures et bien plus encore. Il est principalement utilisé par les environnements de développement intégrés (IDE) pour assurer la prise en charge d’IntelliSense. Il s’intègre parfaitement à Visual Studio Code et est utilisé par des outils tels que Conquer of Completion (Coc).
Les développeurs peuvent exploiter une API dédiée et créer leurs propres plugins personnalisés pour le service de langage afin d’améliorer l’expérience d’édition de TypeScript. Cela peut être particulièrement utile pour mettre en œuvre des fonctionnalités spécifiques de linting ou activer l’autocomplétion pour un langage de gabarits personnalisé.
« typescript-styled-plugin » est un exemple concret de plugin personnalisé : il signale les erreurs de syntaxe et prend en charge IntelliSense pour les propriétés CSS dans les composants stylisés.
Pour obtenir davantage d’informations et consulter des guides de démarrage rapide, vous pouvez vous référer au wiki officiel de TypeScript sur GitHub : https://github.com/microsoft/TypeScript/wiki/
Typage structurel
TypeScript repose sur un système de types structurel. Cela signifie que la compatibilité et l’équivalence des types sont déterminées par leur structure ou leur définition réelle, plutôt que par leur nom ou leur lieu de déclaration, comme c’est le cas dans les systèmes de types nominaux tels que ceux de C# ou C.
Le système de types structurel de TypeScript a été conçu d’après le fonctionnement du typage canard dynamique de JavaScript lors de l’exécution.
L’exemple suivant est du code TypeScript valide. Comme vous pouvez le constater, « X » et « Y » possèdent le même membre « a », bien qu’ils aient des noms de déclaration distincts. Les types sont déterminés par leurs structures et, dans ce cas, puisque celles-ci sont identiques, ils sont compatibles et valides.
type X = { a: string;};type Y = { a: string;};const x: X = { a: 'a' };const y: Y = x; // ValideRègles fondamentales de comparaison de TypeScript
Le processus de comparaison de TypeScript est récursif et s’applique aux types imbriqués à n’importe quel niveau.
Un type « X » est compatible avec « Y » si « Y » possède au moins les mêmes membres que « X ».
type X = { a: string;};const y = { a: 'A', b: 'B' }; // Valide, car il possède au moins les mêmes membres que Xconst r: X = y;Les paramètres de fonction sont comparés selon leur type, et non selon leur nom :
type X = (a: number) => void;type Y = (a: number) => void;let x: X = (j: number) => undefined;let y: Y = (k: number) => undefined;y = x; // Validex = y; // ValideLes types de retour des fonctions doivent être identiques :
type X = (a: number) => undefined;type Y = (a: number) => number;let x: X = (a: number) => undefined;let y: Y = (a: number) => 1;y = x; // Invalidex = y; // InvalideLe type de retour d’une fonction source doit être un sous-type du type de retour d’une fonction cible :
let x = () => ({ a: 'A' });let y = () => ({ a: 'A', b: 'B' });x = y; // Validey = x; // Invalide : le membre b est manquantL’omission de paramètres de fonction est autorisée, car il s’agit d’une pratique courante en JavaScript, par exemple avec « Array.prototype.map() » :
[1, 2, 3].map((element, _index, _array) => element + 'x');Par conséquent, les déclarations de type suivantes sont tout à fait valides :
type X = (a: number) => undefined;type Y = (a: number, b: number) => undefined;let x: X = (a: number) => undefined;let y: Y = (a: number) => undefined; // Paramètre b manquanty = x; // ValideTous les paramètres facultatifs supplémentaires du type source sont valides :
type X = (a: number, b?: number, c?: number) => undefined;type Y = (a: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Validex = y; //ValideTous les paramètres facultatifs du type cible sans paramètres correspondants dans le type source sont valides et ne constituent pas une erreur :
type X = (a: number) => undefined;type Y = (a: number, b?: number) => undefined;let x: X = a => undefined;let y: Y = a => undefined;y = x; // Validex = y; // ValideLe paramètre rest est traité comme une série infinie de paramètres facultatifs :
type X = (a: number, ...rest: number[]) => undefined;let x: X = a => undefined; //valideLes fonctions avec des surcharges sont valides si la signature de surcharge est compatible avec leur signature d’implémentation :
function x(a: string): void;function x(a: string, b: number): void;function x(a: string, b?: number): void { console.log(a, b);}x('a'); // Validex('a', 1); // Valide
function y(a: string): void; // Invalide, incompatible avec la signature d'implémentationfunction y(a: string, b: number): void;function y(a: string, b: number): void { console.log(a, b);}y('a');y('a', 1);La comparaison des paramètres de fonction réussit si les paramètres source et cible peuvent être affectés à des supertypes ou des sous-types (bivariance).
// Supertypeclass X { a: string; constructor(value: string) { this.a = value; }}// Sous-typeclass Y extends X {}// Sous-typeclass Z extends X {}
type GetA = (x: X) => string;const getA: GetA = x => x.a;
// La bivariance accepte les supertypesconsole.log(getA(new X('x'))); // Valideconsole.log(getA(new Y('Y'))); // Valideconsole.log(getA(new Z('z'))); // ValideLes énumérations sont comparables et compatibles avec les nombres, et inversement, mais la comparaison de valeurs d’énumération provenant de types d’énumération différents est invalide.
enum X { A, B,}enum Y { A, B, C,}const xa: number = X.A; // Valideconst ya: Y = 0; // ValideX.A === Y.A; // InvalideLes instances d’une classe sont soumises à une vérification de compatibilité de leurs membres privés et protégés :
class X { public a: string; constructor(value: string) { this.a = value; }}
class Y { private a: string; constructor(value: string) { this.a = value; }}
let x: X = new Y('y'); // InvalideLa comparaison ne tient pas compte des différences dans la hiérarchie d’héritage, par exemple :
class X { public a: string; constructor(value: string) { this.a = value; }}class Y extends X { public a: string; constructor(value: string) { super(value); this.a = value; }}class Z { public a: string; constructor(value: string) { this.a = value; }}let x: X = new X('x');let y: Y = new Y('y');let z: Z = new Z('z');x === y; // Validex === z; // Valide même si z appartient à une autre hiérarchie d'héritageLes génériques sont comparés d’après leurs structures, en fonction du type obtenu après application du paramètre générique ; seul le résultat final est comparé comme un type non générique.
interface X<T> { a: T;}let x: X<number> = { a: 1 };let y: X<string> = { a: 'a' };x === y; // Invalide, car l'argument de type est utilisé dans la structure finaleinterface X<T> {}const x: X<number> = 1;const y: X<string> = 'a';x === y; // Valide, car l'argument de type n'est pas utilisé dans la structure finaleLorsque les arguments de type des génériques ne sont pas spécifiés, tous les arguments non spécifiés sont traités comme des types « any » :
type X = <T>(x: T) => T;type Y = <K>(y: K) => K;let x: X = x => x;let y: Y = y => y;x = y; // ValideÀ retenir :
let a: number = 1;let b: number = 2;a = b; // Valide, tout est assignable à lui-même
let c: any;c = 1; // Valide, tous les types sont assignables à any
let d: unknown;d = 1; // Valide, tous les types sont assignables à unknown
let e: unknown;let e1: unknown = e; // Valide, unknown n'est assignable qu'à lui-même et à anylet e2: any = e; // Validelet e3: number = e; // Invalide
let f: never;f = 1; // Invalide, rien n'est assignable à never
let g: void;let g1: any;g = 1; // Invalide, void n'est assignable à aucun autre type que any, et réciproquementg = g1; // ValideVeuillez noter que lorsque « strictNullChecks » est activé, « null » et « undefined » sont traités de la même manière que « void » ; dans le cas contraire, ils sont semblables à « never ».
Les types comme ensembles
Dans TypeScript, un type est un ensemble de valeurs possibles. Cet ensemble est également appelé le domaine du type. Chaque valeur d’un type peut être considérée comme un élément d’un ensemble. Un type établit les contraintes que chaque élément de l’ensemble doit respecter pour être considéré comme un membre de cet ensemble. La tâche principale de TypeScript consiste à contrôler et à vérifier si un ensemble est un sous-ensemble d’un autre.
TypeScript prend en charge différents types d’ensembles :
| Terme ensembliste | TypeScript | Remarques |
|---|---|---|
| Ensemble vide | never | « never » ne contient rien en dehors de lui-même |
| Ensemble singleton | undefined / null / literal type | |
| Ensemble fini | boolean / union | |
| Ensemble infini | string / number / object | |
| Ensemble universel | any / unknown | Chaque élément appartient à « any » et chaque ensemble en est un sous-ensemble / « unknown » est l’équivalent de « any » qui préserve la sécurité du typage |
Voici quelques exemples :
| TypeScript | Terme ensembliste | Exemple |
|---|---|---|
| never | ∅ (ensemble vide) | const x: never = ‘x’; // Error: Type ‘string’ is not assignable to type ‘never’ |
| Literal type | Ensemble singleton | type X = ‘X’; |
| type Y = 7; | ||
| Valeur affectable à T | Valeur ∈ T (membre de) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| T1 affectable à T2 | T1 ⊆ T2 (sous-ensemble de) | type XY = ‘X’ | ‘Y’; |
| const x: XY = ‘X’; | ||
| const j: XY = ‘J’; // Type ‘“J”’ is not assignable to type ‘XY’. | ||
| T1 extends T2 | T1 ⊆ T2 (sous-ensemble de) | type X = ‘X’ extends string ? true : false; |
| T1 | T2 | T1 ∪ T2 (union) | type XY = ‘X’ | ‘Y’; |
| type JK = 1 | 2; | ||
| T1 & T2 | T1 ∩ T2 (intersection) | type X = { a: string } |
| type Y = { b: string } | ||
| type XY = X & Y | ||
| const x: XY = { a: ‘a’, b: ‘b’ } | ||
| unknown | Ensemble universel | const x: unknown = 1 |
Une union (T1 | T2) crée un ensemble plus large (les deux) :
type X = { a: string;};type Y = { b: string;};type XY = X | Y;const r: XY = { a: 'a', b: 'x' }; // ValideUne intersection (T1 & T2) crée un ensemble plus restreint (uniquement ce qui est partagé) :
type X = { a: string;};type Y = { a: string; b: string;};type XY = X & Y;const r: XY = { a: 'a' }; // Invalideconst j: XY = { a: 'a', b: 'b' }; // ValideDans ce contexte, le mot-clé extends peut être considéré comme signifiant « sous-ensemble de ». Il établit une contrainte pour un type. Lorsque extends est utilisé avec un générique, il limite le paramètre de type générique à un type plus spécifique.
Veuillez noter qu’ici, extends n’a rien à voir avec l’héritage de classes au sens de la POO.
TypeScript utilise des types structurels et ne possède pas de hiérarchie nominale stricte. En réalité, comme dans l’exemple ci-dessous, deux types peuvent se chevaucher sans que l’un soit un sous-type de l’autre, car TypeScript prend en compte la structure, ou forme, des objets.
interface X { a: string;}interface Y extends X { b: string;}interface Z extends Y { c: string;}const z: Z = { a: 'a', b: 'b', c: 'c' };interface X1 { a: string;}interface Y1 { a: string; b: string;}interface Z1 { a: string; b: string; c: string;}const z1: Z1 = { a: 'a', b: 'b', c: 'c' };
const r: Z1 = z; // ValideAttribuer un type : déclarations et assertions de type
Dans TypeScript, un type peut être attribué de différentes manières :
Déclaration de type
Dans l’exemple suivant, nous utilisons x: X (« : Type ») pour déclarer un type pour la variable x.
type X = { a: string;};
// Déclaration de typeconst x: X = { a: 'a',};Si la variable ne respecte pas le format spécifié, TypeScript signale une erreur. Par exemple :
type X = { a: string;};
const x: X = { a: 'a', b: 'b', // Erreur : un littéral objet ne peut spécifier que des propriétés connues};Assertion de type
Il est possible d’ajouter une assertion à l’aide du mot-clé as. Cela indique au compilateur que le développeur dispose de plus d’informations sur un type et supprime toutes les erreurs susceptibles de se produire.
Par exemple :
type X = { a: string;};const x = { a: 'a', b: 'b',} as X;Dans l’exemple ci-dessus, l’objet x fait l’objet d’une assertion de type X à l’aide du mot-clé as. Cela indique au compilateur TypeScript que l’objet est conforme au type spécifié, même s’il possède une propriété supplémentaire b absente de la définition du type.
Les assertions de type sont utiles lorsqu’un type plus spécifique doit être indiqué, notamment lors de l’utilisation du DOM. Par exemple :
const myInput = document.getElementById('my_input') as HTMLInputElement;Ici, l’assertion de type as HTMLInputElement sert à indiquer à TypeScript que le résultat de getElementById doit être traité comme un HTMLInputElement. Les assertions de type peuvent également être utilisées pour remapper des clés, comme le montre l’exemple ci-dessous avec les littéraux de gabarit :
type J<Type> = { [ Property in keyof Type as `prefix_${string & Property}` ]: () => Type[Property];};type X = { a: string; b: number;};type Y = J<X>;Dans cet exemple, le type J<Type> utilise un type mappé avec un littéral de gabarit pour remapper les clés de Type. Il crée de nouvelles propriétés en ajoutant « prefix_ » à chaque clé, et leurs valeurs correspondantes sont des fonctions qui renvoient les valeurs des propriétés d’origine.
Il convient de noter que lors de l’utilisation d’une assertion de type, TypeScript n’effectue pas de vérification des propriétés excédentaires. Il est donc généralement préférable d’utiliser une déclaration de type lorsque la structure de l’objet est connue à l’avance.
Déclarations ambiantes
Les déclarations ambiantes sont des fichiers qui décrivent les types du code JavaScript ; leur nom de fichier respecte le format .d.ts.. Elles sont généralement importées et utilisées pour annoter des bibliothèques JavaScript existantes ou pour ajouter des types aux fichiers JS existants de votre projet.
Les types de nombreuses bibliothèques courantes sont disponibles à l’adresse : https://github.com/DefinitelyTyped/DefinitelyTyped/
et peuvent être installés à l’aide de :
npm install --save-dev @types/library-namePour vos propres déclarations ambiantes, vous pouvez effectuer un import à l’aide de la référence « triple-slash » :
/// <reference path="./library-types.d.ts" />Vous pouvez utiliser des déclarations ambiantes même dans des fichiers JavaScript à l’aide de // @ts-check.
Le mot-clé declare permet de définir des types pour du code JavaScript existant sans l’importer, en servant d’espace réservé aux types provenant d’un autre fichier ou disponibles globalement.
Vérification des propriétés et des propriétés excédentaires
TypeScript repose sur un système de types structurel, mais la vérification des propriétés excédentaires est une fonctionnalité de TypeScript qui lui permet de vérifier si un objet possède exactement les propriétés spécifiées dans le type.
La vérification des propriétés excédentaires est effectuée lors de l’affectation de littéraux d’objet à des variables ou lorsqu’ils sont passés comme arguments à un paramètre de fonction, par exemple.
type X = { a: string;};const y = { a: 'a', b: 'b' };const x: X = y; // Valide grâce au typage structurelconst w: X = { a: 'a', b: 'b' }; // Invalide en raison du contrôle des propriétés excédentairesTypes faibles
Un type est considéré comme faible lorsqu’il ne contient qu’un ensemble de propriétés toutes facultatives :
type X = { a?: string; b?: string;};TypeScript considère comme une erreur l’affectation d’une valeur à un type faible lorsqu’il n’existe aucun chevauchement ; par exemple, le code suivant génère une erreur :
type Options = { a?: string; b?: string;};
const fn = (options: Options) => undefined;
fn({ c: 'c' }); // InvalideBien que cela ne soit pas recommandé, il est possible, si nécessaire, de contourner cette vérification à l’aide d’une assertion de type :
type Options = { a?: string; b?: string;};const fn = (options: Options) => undefined;fn({ c: 'c' } as Options); // ValideOu en ajoutant unknown à la signature d’index du type faible :
type Options = { [prop: string]: unknown; a?: string; b?: string;};
const fn = (options: Options) => undefined;fn({ c: 'c' }); // ValideVérification stricte des littéraux d’objet (fraîcheur)
La vérification stricte des littéraux d’objet, parfois appelée « fraîcheur », est une fonctionnalité de TypeScript qui permet de détecter les propriétés excédentaires ou mal orthographiées qui passeraient autrement inaperçues lors des vérifications structurelles normales des types.
Lors de la création d’un littéral d’objet, le compilateur TypeScript le considère comme « frais ». Si le littéral d’objet est affecté à une variable ou passé en tant que paramètre, TypeScript génère une erreur si le littéral d’objet spécifie des propriétés qui n’existent pas dans le type cible.
Cependant, cette « fraîcheur » disparaît lorsqu’un littéral d’objet est élargi ou qu’une assertion de type est utilisée.
Voici quelques exemples pour illustrer ce comportement :
type X = { a: string };type Y = { a: string; b: string };
let x: X;x = { a: 'a', b: 'b' }; // Contrôle de fraîcheur : affectation invalidevar y: Y;y = { a: 'a', bx: 'bx' }; // Contrôle de fraîcheur : affectation invalide
const fn = (x: X) => console.log(x.a);
fn(x);fn(y); // Élargissement : aucune erreur, types structurellement compatibles
fn({ a: 'a', bx: 'b' }); // Contrôle de fraîcheur : argument invalide
let c: X = { a: 'a' };let d: Y = { a: 'a', b: '' };c = d; // Élargissement : aucun contrôle de fraîcheurInférence de type
TypeScript peut inférer les types lorsqu’aucune annotation n’est fournie lors de :
- L’initialisation d’une variable.
- L’initialisation d’un membre.
- La définition de valeurs par défaut pour les paramètres.
- La détermination du type de retour d’une fonction.
Par exemple :
let x = 'x'; // Le type inféré est stringLe compilateur TypeScript analyse la valeur ou l’expression et détermine son type à partir des informations disponibles.
Inférences plus avancées
Lorsque plusieurs expressions interviennent dans l’inférence de type, TypeScript recherche le « meilleur type commun ». Par exemple :
let x = [1, 'x', 1, null]; // Le type inféré est : (string | number | null)[]Si le compilateur ne peut pas trouver le meilleur type commun, il renvoie un type union. Par exemple :
let x = [new RegExp('x'), new Date()]; // Le type inféré est : (RegExp | Date)[]TypeScript utilise le « typage contextuel » en fonction de l’emplacement de la variable pour inférer les types. Dans l’exemple suivant, le compilateur sait que e est de type MouseEvent grâce au type de l’événement click défini dans le fichier lib.d.ts, qui contient des déclarations ambiantes pour diverses constructions JavaScript courantes et le DOM :
window.addEventListener('click', function (e) {}); // Le type inféré de e est MouseEventÉlargissement des types
L’élargissement de type est le processus par lequel TypeScript attribue un type à une variable initialisée sans annotation de type. Il permet de passer de types étroits à des types plus larges, mais pas l’inverse. Dans l’exemple suivant :
let x = 'x'; // TypeScript infère string, un type largelet y: 'y' | 'x' = 'y'; // Le type de y est une union de types littérauxy = x; // Invalide : le type 'string' n'est pas assignable au type '"x" | "y"'.TypeScript attribue string à x à partir de l’unique valeur fournie lors de l’initialisation (x) ; il s’agit d’un exemple d’élargissement.
TypeScript permet de contrôler le processus d’élargissement, par exemple en utilisant « const ».
Const
L’utilisation du mot-clé const lors de la déclaration d’une variable entraîne une inférence de type plus étroite dans TypeScript.
Par exemple :
const x = 'x'; // TypeScript infère le type de x comme 'x', un type plus étroitlet y: 'y' | 'x' = 'y';y = x; // Valide : le type de x est inféré comme 'x'En déclarant la variable x avec const, son type est restreint à la valeur littérale spécifique ‘x’. Comme le type de x est plus étroit, il peut être affecté à la variable y sans erreur.
Le type peut être inféré ainsi, car les variables const ne peuvent pas être réaffectées. Leur type peut donc être restreint à un type littéral spécifique, ici le type littéral ‘x’.
Modificateur Const sur les paramètres de type
Depuis la version 5.0 de TypeScript, il est possible de spécifier l’attribut const sur un paramètre de type générique. Cela permet d’inférer le type le plus précis possible. Voyons un exemple sans utiliser const :
function identity<T>(value: T) { // Pas de const ici return value;}const values = identity({ a: 'a', b: 'b' }); // Le type inféré est : { a: string; b: string; }Comme vous pouvez le constater, les propriétés a et b sont inférées avec un type string.
Voyons maintenant la différence avec la version utilisant const :
function identity<const T>(value: T) { // Utilisation du modificateur const sur les paramètres de type return value;}const values = identity({ a: 'a', b: 'b' }); // Le type inféré est : { a: "a"; b: "b"; }Nous pouvons maintenant constater que les propriétés a et b sont inférées comme des littéraux de chaîne plutôt que comme de simples types string.
Assertion Const
Cette fonctionnalité permet de déclarer une variable avec un type littéral plus précis à partir de sa valeur d’initialisation, ce qui indique au compilateur que la valeur doit être traitée comme un littéral immuable. Voici quelques exemples :
Sur une seule propriété :
const v = { x: 3 as const,};v.x = 3;Sur un objet entier :
const v = { x: 1, y: 2,} as const;Cela peut être particulièrement utile lors de la définition du type d’un tuple :
const x = [1, 2, 3]; // number[]const y = [1, 2, 3] as const; // Tuple readonly [1, 2, 3]Annotation de type explicite
Nous pouvons être explicites et fournir un type. Dans l’exemple suivant, la propriété x est de type number :
const v = { x: 1, // Type inféré : number (élargissement)};v.x = 3; // ValideNous pouvons rendre l’annotation de type plus précise en utilisant une union de types littéraux :
const v: { x: 1 | 2 | 3 } = { x: 1, // x est maintenant une union de types littéraux : 1 | 2 | 3};v.x = 3; // Validev.x = 100; // InvalideRéduction
La réduction de type est le processus par lequel TypeScript réduit un type général à un type plus spécifique. Cela se produit lorsque TypeScript analyse le code et détermine que certaines conditions ou opérations permettent d’affiner les informations de type.
La réduction des types peut se produire de différentes manières, notamment avec :
Conditions
En utilisant des instructions conditionnelles, telles que if ou switch, TypeScript peut réduire le type en fonction du résultat de la condition. Par exemple :
let x: number | undefined = 10;
if (x !== undefined) { x += 100; // Le type est number, après avoir été affiné par la condition}Lever une exception ou retourner une valeur
Lever une exception ou effectuer un retour anticipé depuis une branche peut aider TypeScript à réduire un type. Par exemple :
let x: number | undefined = 10;
if (x === undefined) { throw 'error';}x += 100;Parmi les autres façons de réduire les types dans TypeScript figurent :
- L’opérateur
instanceof: utilisé pour vérifier si un objet est une instance d’une classe spécifique. - L’opérateur
in: utilisé pour vérifier si une propriété existe dans un objet. - L’opérateur
typeof: utilisé pour vérifier le type d’une valeur lors de l’exécution. - Les fonctions intégrées telles que
Array.isArray(): utilisées pour vérifier si une valeur est un tableau.
Union discriminée
L’utilisation d’une « union discriminée » est un modèle dans TypeScript où une « étiquette » explicite est ajoutée aux objets afin de distinguer les différents types d’une union. Ce modèle est également appelé « union étiquetée ». Dans l’exemple suivant, l’« étiquette » est représentée par la propriété « type » :
type A = { type: 'type_a'; value: number };type B = { type: 'type_b'; value: string };
const x = (input: A | B): string | number => { switch (input.type) { case 'type_a': return input.value + 100; // le type est A case 'type_b': return input.value + 'extra'; // le type est B }};Gardes de type définies par l’utilisateur
Lorsque TypeScript ne parvient pas à déterminer un type, il est possible d’écrire une fonction auxiliaire appelée « garde de type définie par l’utilisateur ». Dans l’exemple suivant, nous utiliserons un prédicat de type pour réduire le type après l’application d’un filtrage :
const data = ['a', null, 'c', 'd', null, 'f'];
const r1 = data.filter(x => x != null); // Le type est (string | null)[], TypeScript n'a pas pu l'inférer correctement
const isValid = (item: string | null): item is string => item !== null; // Garde de type personnalisé
const r2 = data.filter(isValid); // Le type est maintenant correctement défini comme string[], grâce au garde de type à prédicat qui a permis de l'affinerRéduction avec switch-true
TypeScript 5.3 ajoute la réduction avec switch-true, qui permet de remplacer des chaînes if/else peu lisibles par switch (true) utilisant des conditions booléennes. Elle améliore la lisibilité tout en continuant à réduire les types. Elle s’apparente à la correspondance de motifs, mais en plus simple.
function classify(x: unknown) { switch (true) { case typeof x === 'string': return `"${x.toUpperCase()}"`; case typeof x === 'number': return x > 0 ? 'positive' : 'negative'; case Array.isArray(x): return `[${x.length} items]`; default: return 'something else'; }}