
Microsoft vient de publier la version bêta de TypeScript 5.8. Cette version améliore la façon dont les types de retour sont vérifiés pour les types d'accès conditionnels et indexés, optimise l'interopérabilité des modules dans Node.js, et inclut des améliorations de performance pour une résolution plus rapide des projets.
Pour commencer à utiliser la version bêta, vous pouvez l'obtenir via npm avec la commande suivante :
Code : | Sélectionner tout |
npm install -D typescript@beta
Retours vérifiés pour les types d'accès conditionnels et indexés
Considérons une API qui présente un ensemble d'options à un utilisateur :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | /** * @param prompt The text to show to a user. * @param selectionKind Whether a user can select multiple options, or just a single option. * @param items Each of the options presented to the user. **/ async function showQuickPick( prompt: string, selectionKind: SelectionKind, items: readonly string[], ): Promise<string | string[]> { // ... } enum SelectionKind { Single, Multiple, } |
L'objectif de showQuickPick est d'afficher un élément d'interface utilisateur qui permet de sélectionner une ou plusieurs options. Le moment où il le fait est déterminé par le paramètre selectionKind. Lorsque selectionKind est SelectionKind.Single, le type de retour de showQuickPick doit être string, et lorsqu'il est SelectionKind.Multiple, le type de retour doit être string[].
Le problème est que la signature de type de showQuickPick n'est pas claire. Elle indique simplement que le type retourné est string | string[] - il pourrait s'agir d'un string ou d'une string[], mais l'appelant doit vérifier explicitement. Dans l'exemple ci-dessous, on pourrait s'attendre à ce que shoppingList ait le type string[], mais on se retrouve avec le type plus large string | string[].
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | let shoppingList = await showQuickPick( "Which fruits do you want to purchase?", SelectionKind.Multiple, ["apples", "oranges", "bananas", "durian"], ); console.log(`Alright, going out to buy some ${shoppingList.join(", ")}`); // ~~~~ // error! // Property 'join' does not exist on type 'string | string[]'. // Property 'join' does not exist on type 'string'. |
Au lieu de cela, nous pouvons utiliser un type conditionnel pour rendre le type de retour de showQuickPick plus précis :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple ? string[] : string async function showQuickPick<S extends SelectionKind>( prompt: string, selectionKind: S, items: readonly string[], ): Promise<QuickPickReturn<S>> { // ... } |
Cela fonctionne bien pour les appelants.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // `SelectionKind.Multiple` gives a `string[]` - works let shoppingList: string[] = await showQuickPick( "Which fruits do you want to purchase?", SelectionKind.Multiple, ["apples", "oranges", "bananas", "durian"], ); // `SelectionKind.Single` gives a `string` - works let dinner: string = await showQuickPick( "What's for dinner tonight?", SelectionKind.Single, ["sushi", "pasta", "tacos", "ugh I'm too hungry to think, whatever you want"], ); |
Mais qu'en est-il si nous essayons d'implémenter showQuickPick ?
Code : | 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 26 27 28 29 30 31 32 33 34 35 36 | async function showQuickPick<S extends SelectionKind>( prompt: string, selectionKind: S, items: readonly string[], ): Promise<QuickPickReturn<S>> { if (items.length < 1) { throw new Error("At least one item must be provided."); } // Create buttons for every option. let buttons = items.map(item => ({ selected: false, text: item, })); // Default to the first element if necessary. if (selectionKind === SelectionKind.Single) { buttons[0].selected = true; } // Event handling code goes here... // Figure out the selected items const selectedItems = buttons .filter(button => button.selected) .map(button => button.text); if (selectionKind === SelectionKind.Single) { // Pick the first (only) selected item. return selectedItems[0]; } else { // Return all selected items. return selectedItems; } } |
Malheureusement, TypeScript émet une erreur sur chacune des instructions de retour.
Code : | Sélectionner tout |
1 2 | Type 'string[]' is not assignable to type 'QuickPickReturn<S>'. Type 'string' is not assignable to type 'QuickPickReturn<S>'. |
Jusqu'à présent, TypeScript exigeait une assertion de type pour implémenter toute fonction renvoyant un type conditionnel d'ordre supérieur.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | if (selectionKind === SelectionKind.Single) { // Pick the first (only) selected item. - return selectedItems[0]; + return selectedItems[0] as QuickPickReturn<S>; } else { // Return all selected items. - return selectedItems; + return selectedItems as QuickPickReturn<S>; } |
Cette situation n'est pas idéale car les assertions de type annulent les vérifications légitimes que TypeScript effectuerait autrement. Par exemple, il serait idéal que TypeScript puisse détecter le bogue suivant où chaque branche du if/else est mélangée :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | if (selectionKind === SelectionKind.Single) { // Oops! Returning an array when the caller expects a single item! return selectedItems; } else { // Oops! Returning a single item when the caller expects an array! return selectedItems[0]; } |
Pour éviter les assertions de type, TypeScript 5.8 prend désormais en charge une forme limitée de vérification des types conditionnels dans les instructions de retour. Lorsque le type de retour d'une fonction est un type conditionnel générique, TypeScript utilise désormais l'analyse du flux de contrôle pour les paramètres génériques dont les types sont utilisés dans le type conditionnel, instancie le type conditionnel avec le type réduit de chaque paramètre, et effectue une vérification par rapport à ce nouveau type.
Qu'est-ce que cela signifie en pratique ? Tout d'abord, examinons quels genres de types conditionnels impliquent une réduction. Pour refléter la façon dont la réduction opère dans les expressions, nous devons être plus explicites et exhaustifs sur ce qui se passe dans chaque branche.
Code : | Sélectionner tout |
1 2 3 4 | type QuickPickReturn<S extends SelectionKind> = S extends SelectionKind.Multiple ? string[] : S extends SelectionKind.Single ? string : never; |
Une fois que c'est fait, tout fonctionne dans l'exemple que nous venons de donner. Les appelants n'ont aucun problème, et l'implémentation est maintenant sûre. Et si l'on essaie d'intervertir le contenu des branches du if, TypeScript le signale correctement comme une erreur.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | if (selectionKind === SelectionKind.Single) { // Oops! Returning an array when the caller expects a single item! return selectedItems; // ~~~~~~ // error! Type 'string[]' is not assignable to type 'string'. } else { // Oops! Returning a single item when the caller expects an array! return selectedItems[0]; // ~~~~~~ // error! Type 'string[]' is not assignable to type 'string'. } |
Notez que TypeScript fait désormais quelque chose de similaire si l'on utilise des types d'accès indexés. Au lieu d'un type conditionnel, il est possible d'utiliser un type qui agit comme un plan de SelectionKind vers le type de retour que l'on souhaite :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | interface QuickPickReturn { [SelectionKind.Single]: string; [SelectionKind.Multiple]: string[]; } async function showQuickPick<S extends SelectionKind>( prompt: string, selectionKind: S, items: readonly string[], ): Promise<QuickPickReturn[S]> { // ... } |
Pour de nombreux utilisateurs, il s'agira d'une manière plus ergonomique d'écrire le même code. Toutefois, cette fonctionnalité présente certaines limitations.
Prise en charge de require() pour les modules ECMAScript dans --module nodenext
Pendant des années, Node.js a supporté les modules ECMAScript (ESM) en même temps que les modules CommonJS. Malheureusement, l'interopérabilité entre les deux a posé quelques problèmes.
- Les fichiers ESM pouvaient utiliser import sur des fichiers CommonJS
- Les fichiers CommonJS ne pouvaient pas utiliser require() pour les fichiers ESM
En d'autres termes, il était possible de consommer des fichiers CommonJS à partir de fichiers ESM, mais pas l'inverse. Cela a posé de nombreux problèmes aux auteurs de bibliothèques qui souhaitaient fournir un support ESM. Ces auteurs devaient soit rompre la compatibilité avec les utilisateurs de CommonJS, soit « publier deux fois » leurs bibliothèques (en fournissant des points d'entrée distincts pour ESM et CommonJS), soit rester indéfiniment sur CommonJS. Bien que la double publication puisse sembler être un bon compromis, il s'agit d'un processus complexe et sujet aux erreurs, qui double également la quantité de code dans un paquetage.
Node.js 22 assouplit certaines de ces restrictions et autorise les appels require("esm") des modules CommonJS vers les modules ECMAScript. Node.js n'autorise toujours pas l'utilisation de require() pour les fichiers ESM qui contiennent un await de premier niveau, mais la plupart des autres fichiers ESM peuvent désormais être consommés à partir de fichiers CommonJS. Cela représente une opportunité majeure pour les auteurs de bibliothèques de fournir un support ESM sans avoir à publier leurs bibliothèques en double.
TypeScript 5.8 supporte ce comportement avec l'option --module nodenext. Lorsque --module nodenext est activé, TypeScript évitera d'émettre des erreurs sur ces appels require() aux fichiers ESM.
Parce que cette fonctionnalité peut être rétroportée vers des versions plus anciennes de Node.js, il n'y a actuellement pas d'option stable --module nodeXXXX qui permette ce comportement ; cependant, il est prévu que les futures versions de TypeScript puissent stabiliser cette fonctionnalité sous node20. En attendant, les utilisateurs de Node.js 22 et plus récents sont encouragés à utiliser --module nodenext, tandis que les auteurs de bibliothèques et les utilisateurs de versions plus anciennes de Node.js devraient continuer à utiliser --module node16 (ou faire la mise à jour mineure vers --module node18).
--module node18
TypeScript 5.8 introduit un flag stable --module node18. Pour les utilisateurs qui sont fixés sur l'utilisation de Node.js 18, ce drapeau fournit un point de référence stable qui n'incorpore pas certains comportements qui sont dans --module nodenext. En particulier :
- require() des modules ECMAScript est interdit sous node18, mais autorisé sous nodenext
- les assertions import (dépréciées en faveur des attributs import) sont autorisées sous node18, mais ne sont pas autorisées sous nodenext
L'option --erasableSyntaxOnly
Récemment, Node.js 23.6 a supprimé le support expérimental pour l'exécution directe de fichiers TypeScript ; cependant, seules certaines constructions sont supportées dans ce mode. Node.js a supprimé un mode appelé --experimental-strip-types qui exige que toute syntaxe spécifique à TypeScript ne puisse pas avoir de sémantique d'exécution. En d'autres termes, il doit être possible d'effacer facilement toute syntaxe spécifique à TypeScript d'un fichier, en laissant un fichier JavaScript valide.
Cela signifie que des constructions comme celles qui suivent ne sont pas prises en charge :
- déclarations enum
- namespaces et modules avec code runtime
- propriétés des paramètres dans les classes
- alias import
Des outils similaires comme ts-blank-space ou Amaro (la bibliothèque sous-jacente pour la séparation des types dans Node.js) ont les mêmes limitations. Ces outils fourniront des messages d'erreur utiles s'ils rencontrent du code qui ne répond pas à ces exigences, mais vous ne découvrirez toujours pas que votre code ne fonctionne pas tant que vous n'aurez pas essayé de l'exécuter.
C'est pourquoi TypeScript 5.8 introduit l'option --erasableSyntaxOnly. Lorsque ce drapeau est activé, TypeScript ne vous permet d'utiliser que des constructions qui peuvent être effacées d'un fichier, et émet une erreur s'il rencontre des constructions qui ne peuvent pas être effacées.
Code : | Sélectionner tout |
1 2 3 4 5 6 | class C { constructor(public x: number) { } // ~~~~~~~~~~~~~~~~ // error! This syntax is not allowed when 'erasableSyntaxOnly' is enabled. } } |
Le flag --libReplacement
Dans TypeScript 4.5, la possibilité de remplacer les fichiers lib par défaut par des fichiers personnalisés a été introduite. Ceci était basé sur la possibilité de résoudre un fichier de bibliothèque à partir de paquets nommés @typescript/lib-*. Par exemple, vous pouvez verrouiller vos bibliothèques dom sur une version spécifique du package @types/web avec le package.json suivant :
Code : | Sélectionner tout |
1 2 3 4 5 | { "devDependencies": { "@typescript/lib-dom": "npm:@types/web@0.0.199" } } |
Une fois installé, un paquetage appelé @typescript/lib-dom devrait exister, et TypeScript le recherchera toujours lorsque dom est impliqué par vos paramètres.
Il s'agit d'une fonctionnalité puissante, mais qui implique également un peu de travail supplémentaire. Même si vous n'utilisez pas cette fonctionnalité, TypeScript effectue toujours cette recherche et doit surveiller les changements dans node_modules au cas où un paquetage de remplacement lib commencerait à exister.
TypeScript 5.8 introduit l'option --libReplacement, qui permet de désactiver ce comportement. Si vous n'utilisez pas --libReplacement, vous pouvez maintenant le désactiver avec --libReplacement false. Dans le futur, --libReplacement false pourrait devenir la valeur par défaut, donc si vous utilisez actuellement ce comportement, vous devriez envisager de l'activer explicitement avec --libReplacement true.
Conservation des noms de propriété calculés dans les fichiers de déclaration
Afin de rendre l'émission des propriétés calculées plus prévisible dans les fichiers de déclaration, TypeScript 5.8 préservera systématiquement les noms d'entités (bareVariables et dotted.names.that.look.like.this) dans les noms de propriétés calculées dans les classes.
Par exemple, considérons le code suivant :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | export let propName = "theAnswer"; export class MyClass { [propName] = 42; // ~~~~~~~~~~ // error! // A computed property name in a class property declaration must have a simple literal type or a 'unique symbol' type. } |
Les versions précédentes de TypeScript émettaient une erreur lors de la génération d'un fichier de déclaration pour ce module, et un fichier de déclaration au mieux de sa forme générait une signature d'index.
Code : | Sélectionner tout |
1 2 3 4 | export declare let propName: string; export declare class MyClass { [x: string]: number; } |
Dans TypeScript 5.8, l'exemple de code est désormais autorisé et le fichier de déclaration généré correspondra à ce que vous avez écrit :
Code : | Sélectionner tout |
1 2 3 4 | export declare let propName: string; export declare class MyClass { [propName]: number; } |
Notez que cela ne crée pas de propriétés nommées statiquement sur la classe. Vous obtiendrez toujours ce qui est en fait une signature d'index comme [x : string] : number, donc pour ce cas d'utilisation, vous devrez utiliser des unique symbol ou des types littéraux.
Notez que l'écriture de ce code était et est toujours une erreur sous le flag --isolatedDeclarations ;...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.