Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

TypeScript 4.1 apporte le remappage de clé dans les types mappés, les types template de littéraux,
Les types conditionnels récursifs et bien d'autres

Le , par Stéphane le calme

31PARTAGES

10  1 
Microsoft a annoncé la disponibilité de TypeScript 4.1, la première mise à niveau de TypeScript 4.0. Au sujet de cette dernière (TypeScript 4.0), Microsoft avait déclaré :

« Cette version du langage représente notre prochaine génération de versions de TypeScript, alors que nous approfondissons l'expressivité, la productivité et l'évolutivité.

« Si vous n'êtes pas familier avec TypeScript, c'est un langage qui s'appuie sur JavaScript en ajoutant une syntaxe pour les types statiques. L'idée est qu'en écrivant les types de vos valeurs et où elles sont utilisées, vous pouvez utiliser TypeScript pour vérifier le type de votre code et vous informer des erreurs avant d'exécuter votre code (et même avant d'enregistrer votre fichier). Vous pouvez ensuite utiliser le compilateur TypeScript pour supprimer les types de votre code et vous laisser avec un JavaScript propre et lisible qui s'exécute n'importe où. Au-delà de la vérification, TypeScript utilise également des types statiques pour alimenter d'excellents outils d'édition tels que l'autocomplétion, la navigation dans le code, les refactorisations, etc. En fait, si vous avez utilisé JavaScript dans un éditeur tel que Visual Studio Code ou Visual Studio, vous avez déjà utilisé une expérience optimisée par types et TypeScript.

« Avec TypeScript 4.0, il n'y a pas de changements majeurs de rupture. En fait, si vous êtes nouveau dans le langage, c'est le meilleur moment pour commencer à l'utiliser. La communauté est déjà là et grandit, avec un code fonctionnel et de nouvelles ressources à apprendre. Et une chose à garder à l'esprit: malgré toutes les bonnes choses que nous apportons dans la version 4.0, il vous suffit de connaître les bases de TypeScript pour être productif! »

Types template de littéraux

Les types littéraux de chaîne de caractères dans TypeScript nous permettent de modéliser des fonctions et des API qui attendent un ensemble de chaînes spécifiques de caractères.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
function setVerticalAlignment(pos: "top" | "middle" | "bottom") {
    // ...
}
 
setVerticalAlignment("middel");
//                   ~~~~~~~~
// error: Argument of type '"middel"' is not assignable to
//        parameter of type '"top" | "middle" | "bottom"'.

C'est plutôt bien, car les types littéraux de chaîne de caractères peuvent essentiellement vérifier l'orthographe de nos valeurs de chaîne.

Certains développeurs aiment également que les littéraux de chaîne de caractères puissent être utilisés comme noms de propriété dans les types mappés. En ce sens, ils peuvent également être utilisés comme éléments de base.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
type Options = {
    [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   };

Mais il existe un autre cas où ces types littéraux de chaîne de caractères peuvent être utilisés comme blocs de construction : la construction d'autres types littéraux de chaîne de caractères.

C’est pourquoi TypeScript 4.1 apporte le type template de littéraux de chaîne. Il a la même syntaxe que les templates de littéraux de chaînes de caractères en JavaScript, mais est utilisé dans les positions de type. Lorsque vous l'utilisez avec des types littéraux concrets, il produit un nouveau type littéral de chaîne en concaténant le contenu.

Code TypeScript : Sélectionner tout
1
2
3
4
5
type World = "world";
 
type Greeting = `hello ${World}`;
// same as
//   type Greeting = "hello world";

Que se passe-t-il lorsque des unions occupent des positions de substitution ? Il produit l'ensemble de tous les littéraux de chaîne possibles qui pourraient être représentés par chaque membre d'union.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
type Color = "red" | "blue";
type Quantity = "one" | "two";
 
type SeussFish = `${Quantity | Color} fish`;
// same as
//   type SeussFish = "one fish" | "two fish"
//                  | "red fish" | "blue fish";

Cela peut être utilisé au-delà des exemples dans les notes de publication. Par exemple, plusieurs bibliothèques de composants d'interface utilisateur ont un moyen de spécifier un alignement vertical et horizontal dans leurs API, souvent les deux à la fois en utilisant une seule chaîne comme "bottom-right" (en bas à droite). Entre l'alignement vertical avec "top" (haut), "middle" (milieu) et "bottom" (bas) et l'alignement horizontal avec "left", "center" et "right", il y a 9 chaînes possibles où chacune des anciennes chaînes est connectée à chacune des les dernières chaînes en utilisant un tiret.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
type VerticalAlignment = "top" | "middle" | "bottom";
type HorizontalAlignment = "left" | "center" | "right";
 
// Takes
//   | "top-left"    | "top-center"    | "top-right"
//   | "middle-left" | "middle-center" | "middle-right"
//   | "bottom-left" | "bottom-center" | "bottom-right"
declare function setAlignment(value: `${VerticalAlignment}-${HorizontalAlignment}`): void;
 
setAlignment("top-left");   // works!
setAlignment("top-middel"); // error!
setAlignment("top-pot");    // error! but good doughnuts if you're ever in Seattle

Bien qu'il existe de nombreux exemples de ce type d'API, il s'agit toujours d'exemple assez simple dans la mesure où nous pourrions les écrire manuellement. En fait, pour 9 chaînes, c'est probablement très bien; mais lorsque vous avez besoin d'une tonne de chaînes, vous devriez envisager de les générer automatiquement à l'avance pour économiser du travail sur chaque vérification de type (ou simplement utiliser un string, ce qui sera beaucoup plus simple à comprendre).

Une partie de la valeur réelle provient de la création dynamique de nouveaux littéraux de chaîne. Par exemple, imaginez une API makeWatchedObject qui prend un objet et produit un objet pratiquement identique, mais avec une nouvelle méthode on pour détecter les modifications des propriétés.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
let person = makeWatchedObject({
    firstName: "Homer",
    age: 42, // give-or-take
    location: "Springfield",
});
 
person.on("firstNameChanged", () => {
    console.log(`firstName was changed!`);
});

Notez que on écoute l'événement "firstNameChanged", pas seulement "firstName". Comment écririons-nous cela?

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
type PropEventSource<T> = {
    on(eventName: `${string & keyof T}Changed`, callback: () => void): void;
};
 
/// Create a "watched object" with an 'on' method
/// so that you can watch for changes to properties.
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;

Avec cela, nous pouvons créer quelque chose qui se trompe lorsque nous attribuons la mauvaise propriété!

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
// error!
person.on("firstName", () => {
});
 
// error!
person.on("frstNameChanged", () => {
});

Nous pouvons également faire quelque chose de spécial dans les types template de littéraux : nous pouvons déduire des positions de substitution. Nous pouvons rendre notre dernier exemple générique pour déduire des parties de la chaîne eventName pour déterminer la propriété associée.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
type PropEventSource<T> = {
    on<K extends string & keyof T>
        (eventName: `${K}Changed`, callback: (newValue: T[K]) => void ): void;
};
 
declare function makeWatchedObject<T>(obj: T): T & PropEventSource<T>;
 
let person = makeWatchedObject({
    firstName: "Homer",
    age: 42,
    location: "Springfield",
});
 
// works! 'newName' is typed as 'string'
person.on("firstNameChanged", newName => {
    // 'newName' has the type of 'firstName'
    console.log(`new name is ${newName.toUpperCase()}`);
});
 
// works! 'newAge' is typed as 'number'
person.on("ageChanged", newAge => {
    if (newAge < 0) {
        console.log("warning! negative age");
    }
})

Ici, nous avons transformé on en méthode générique. Lorsqu'un utilisateur fait un appel avec la chaîne «firstNameChanged», TypeScript essaiera de déduire le bon type pour K. Pour ce faire, il comparera K au contenu antérieur à «Changed» et en déduira la chaîne «firstName». Une fois que TypeScript l'a déterminé, la méthode on peut récupérer le type de firstName sur l'objet d'origine, qui est string dans ce cas. De même, lorsque nous faisons un appel avec "ageChanged", elle trouve le type de la propriété age qui est number.

L'inférence peut être combinée de différentes manières, souvent pour déconstruire des chaînes et les reconstruire de différentes manières. En fait, pour vous aider à modifier ces types littéraux de chaîne, l'équipe a ajouté quelques nouveaux alias de type d'utilitaire pour modifier la casse des lettres (c'est-à-dire la conversion en caractères minuscules et majuscules).

Code TypeScript : Sélectionner tout
1
2
3
4
5
type EnthusiasticGreeting<T extends string> = `${Uppercase<T>}`
 
type HELLO = EnthusiasticGreeting<"hello">;
// same as
//   type HELLO = "HELLO";

Les nouveaux alias de type sont Uppercase (majuscule), Lowercase (minuscule), Capitalize et Uncapitalize. Les deux premiers transforment chaque caractère d'une chaîne et les deux derniers ne transforment que le premier caractère d'une chaîne.

Remappage de clé dans les types mappés

Pour rappel, un type mappé peut créer de nouveaux types d'objets basés sur des clés arbitraires :

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
type Options = {
    [K in "noImplicitAny" | "strictNullChecks" | "strictFunctionTypes"]?: boolean
};
// same as
//   type Options = {
//       noImplicitAny?: boolean,
//       strictNullChecks?: boolean,
//       strictFunctionTypes?: boolean
//   };

ou de nouveaux types d'objets basés sur d'autres types d'objets :

Code TypeScript : Sélectionner tout
1
2
3
4
/// 'Partial<T>' is the same as 'T', but with each property marked optional.
type Partial<T> = {
    [K in keyof T]?: T[K]
};

Jusqu'à présent, les types mappés ne pouvaient produire que de nouveaux types d'objets avec les clés que vous leur fournissiez; cependant, vous voulez souvent pouvoir créer de nouvelles clés ou filtrer les clés en fonction des entrées.

C’est pourquoi TypeScript 4.1 vous permet de remapper les clés dans les types mappés avec une nouvelle clause as.

Code TypeScript : Sélectionner tout
1
2
3
4
5
type MappedTypeWithNewKeys<T> = {
    [K in keyof T as NewKeyType]: T[K]
    //            ^^^^^^^^^^^^^
    //            This is the new syntax!
}

Avec cette nouvelle clause as, vous pouvez tirer parti de fonctionnalités telles que les types template de littéraux pour créer facilement des noms de propriété basés sur d'anciens.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
type Getters<T> = {
    [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
 
interface Person {
    name: string;
    age: number;
    location: string;
}
 
type LazyPerson = Getters<Person>;

et vous pouvez même filtrer les clés en produisant never. Cela signifie que vous n’avez pas besoin d’utiliser un type d’assistant Omit supplémentaire dans certains cas.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Remove the 'kind' property
type RemoveKindField<T> = {
    [K in keyof T as Exclude<K, "kind">]: T[K]
};
 
interface Circle {
    kind: "circle";
    radius: number;
}
 
type KindlessCircle = RemoveKindField<Circle>;
// same as
//   type KindlessCircle = {
//       radius: number;
//   };

Types conditionnels récursifs

En JavaScript, il est assez courant de voir des fonctions qui peuvent aplatir et créer des types de conteneurs à des niveaux arbitraires. Par exemple, considérons la méthode .then() sur les instances de Promise. .then(...) décompresse chaque Promise jusqu'à ce qu'il trouve une valeur qui ne ressemble pas à une Promise et transmet cette valeur à une fonction de rappel (callback). Il existe également une méthode flat relativement nouvelle sur les Array qui peut prendre une profondeur d'aplatissement.

Exprimer cela dans le système de types de TypeScript n’était, à toutes fins pratiques, pas possible. Bien qu'il y ait eu des hacks pour y parvenir, les types ont fini par sembler très déraisonnables.

C’est pourquoi TypeScript 4.1 assouplit certaines restrictions sur les types conditionnels, afin qu’ils puissent modéliser ces modèles. Dans TypeScript 4.1, les types conditionnels peuvent désormais se référencer immédiatement dans leurs branches, ce qui facilite l'écriture d'alias de type récursif.

Par exemple, si nous voulions écrire un type pour obtenir les types d'éléments des tableaux imbriqués, nous pourrions écrire le type deepFlatten suivant.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
type ElementType<T> =
    T extends ReadonlyArray<infer U> ? ElementType<U> : T;
 
function deepFlatten<T extends readonly unknown[]>(x: T): ElementType<T>[] {
    throw "not implemented";
}
 
// All of these return the type 'number[]':
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);

De même, dans TypeScript 4.1, nous pouvons écrire un type Awaited pour développer profondément les Promise.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
 
/// Like `promise.then(...)`, but more accurate in types.
declare function customThen<T, U>(
    p: Promise<T>,
    onFulfilled: (value: Awaited<T>) => U
): Promise<Awaited<U>>;

Gardez à l'esprit que si ces types récursifs sont puissants, ils doivent être utilisés de manière responsable et avec parcimonie.

Tout d'abord, ces types peuvent faire beaucoup de travail, ce qui signifie qu'ils peuvent augmenter le temps de vérification de type. Essayer de numéros de modèle dans la conjecture de Collatz ou la séquence de Fibonacci peut être amusant, mais ne les livrez pas dans des fichiers .d.ts sur npm.

En plus d'être gourmand en puissance de calcul, ces types peuvent atteindre une limite de profondeur de récursivité interne sur des entrées suffisamment complexes. Lorsque cette limite de récursivité est atteinte, cela entraîne une erreur de compilation. En général, il vaut mieux ne pas utiliser du tout ces types que d’écrire quelque chose qui échoue sur des exemples plus réalistes.

Source : Microsoft

Une erreur dans cette actualité ? Signalez-le nous !