Préservation du rétrécissement dans les fermetures après les dernières affectations
TypeScript peut généralement déterminer un type plus spécifique pour une variable en se basant sur les vérifications que vous pouvez effectuer. Ce processus est appelé rétrécissement.
Code : | Sélectionner tout |
1 2 3 4 5 6 | function uppercaseStrings(x: string | number) { if (typeof x === "string") { // TypeScript knows 'x' is a 'string' here. return x.toUpperCase(); } } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function getUrls(url: string | URL, names: string[]) { if (typeof url === "string") { url = new URL(url); } return names.map(name => { url.searchParams.set("name", name) // ~~~~~~~~~~~~ // error! // Property 'searchParams' does not exist on type 'string | URL'. return url.toString(); }); } |
TypeScript 5.4 tire parti de cette situation pour rendre le rétrécissement un peu plus intelligent. Lorsque des paramètres et des variables let sont utilisés dans des fonctions non hoisies, le vérificateur de type recherche un dernier point d'affectation. S'il en trouve un, TypeScript peut, en toute sécurité, rétrécir à partir de l'extérieur de la fonction contenante. Cela signifie que l'exemple ci-dessus fonctionne maintenant.
Notez que l'analyse de rétrécissement n'intervient pas si la variable est assignée n'importe où dans une fonction imbriquée. En effet, il n'y a aucun moyen de savoir avec certitude si la fonction sera appelée plus tard.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function printValueLater(value: string | undefined) { if (value === undefined) { value = "missing!"; } setTimeout(() => { // Modifying 'value', even in a way that shouldn't affect // its type, will invalidate type refinements in closures. value = value; }, 500); setTimeout(() => { console.log(value.toUpperCase()); // ~~~~~ // error! 'value' is possibly 'undefined'. }, 1000); } |
Le type utilitaire NoInfer
Lors de l'appel de fonctions génériques, TypeScript est capable de déduire le type des arguments à partir de ce que vous lui passez.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | function doSomething<T>(arg: T) { // ... } // We can explicitly say that 'T' should be 'string'. doSomething<string>("hello!"); // We can also just let the type of 'T' get inferred. doSomething("hello!"); |
Par exemple, imaginons une fonction createStreetLight qui prend une liste de noms de couleurs, ainsi qu'une couleur par défaut optionnelle.
Code : | Sélectionner tout |
1 2 3 4 5 | function createStreetLight<C extends string>(colors: C[], defaultColor?: C) { // ... } createStreetLight(["red", "yellow", "green"], "red"); |
Code : | Sélectionner tout |
1 2 | // Oops! This undesirable, but is allowed! createStreetLight(["red", "yellow", "green"], "blue"); |
L'une des façons de résoudre ce problème est d'ajouter un paramètre de type séparé qui est délimité par le paramètre de type existant.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | function createStreetLight<C extends string, D extends C>(colors: C[], defaultColor?: D) { } createStreetLight(["red", "yellow", "green"], "blue"); // ~~~~~~ // error! // Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'. |
C'est pourquoi TypeScript 5.4 introduit un nouveau type utilitaire NoInfer<T>. Entourer un type de NoInfer<...> indique à TypeScript de ne pas creuser et de ne pas comparer les types internes pour trouver des candidats à l'inférence de type.
En utilisant NoInfer, on peut réécrire createStreetLight comme suit :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | function createStreetLight<C extends string>(colors: C[], defaultColor?: NoInfer<C>) { // ... } createStreetLight(["red", "yellow", "green"], "blue"); // ~~~~~~ // error! // Argument of type '"blue"' is not assignable to parameter of type '"red" | "yellow" | "green" | undefined'. |
Object.groupBy et Map.groupBy
TypeScript 5.4 ajoute des déclarations pour les nouvelles méthodes statiques JavaScript Object.groupBy et Map.groupBy.
Object.groupBy prend un itérable et une fonction qui décide dans quel "groupe" chaque élément doit être placé. La fonction doit créer une "clé" pour chaque groupe distinct, et Object.groupBy utilise cette clé pour créer un objet où chaque clé correspond à un tableau contenant l'élément original.
Voici donc le JavaScript suivant :
Code : | Sélectionner tout |
1 2 3 4 5 | const array = [0, 1, 2, 3, 4, 5]; const myObj = Object.groupBy(array, (num, index) => { return num % 2 === 0 ? "even": "odd"; }); |
Code : | Sélectionner tout |
1 2 3 4 | const myObj = { even: [0, 2, 4], odd: [1, 3, 5], }; |
Code : | Sélectionner tout |
1 2 3 | const myObj = Map.groupBy(array, (num, index) => { return num % 2 === 0 ? "even" : "odd"; }); |
Code : | Sélectionner tout |
1 2 3 4 | const myObj = new Map(); myObj.set("even", [0, 2, 4]); myObj.set("odd", [1, 3, 5]); |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | interface EvenOdds { even?: number[]; odd?: number[]; } const myObj: EvenOdds = Object.groupBy(...); myObj.even; // ~~~~ // Error to access this under 'strictNullChecks'. |
Notez également que ces méthodes ne sont accessibles qu'en configurant target à esnext ou en ajustant les paramètres de lib. Microsoft espère qu'elles seront éventuellement disponibles sous une cible stable es2024.
Support des appels require() dans --moduleResolution bundler et --module preserve
TypeScript dispose d'une option moduleResolution appelée bundler qui est censée modéliser la façon dont les bundlers modernes déterminent à quel fichier un chemin d'importation fait référence. L'une des limitations de cette option est qu'elle doit être associée à --module esnext, ce qui rend impossible l'utilisation de la syntaxe import ... = require(...).
Code : | Sélectionner tout |
1 2 | // previously errored import myModule = require("module/path"); |
Dans TypeScript 5.4, require() peut désormais être utilisé pour définir le paramètre de module avec une nouvelle option appelée preserve.
Entre --module preserve et --moduleResolution bundler, les deux modélisent plus précisément ce que les bundlers et les runtimes comme Bun autoriseront, et comment ils effectueront les recherches de modules. En fait, en utilisant --module preserve, l'option bundler sera implicitement définie pour --moduleResolution (avec --esModuleInterop et --resolveJsonModule).
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | { "compilerOptions": { "module": "preserve", // ^ also implies: // "moduleResolution": "bundler", // "esModuleInterop": true, // "resolveJsonModule": true, // ... } } |
Code : | Sélectionner tout |
1 2 | import * as foo from "some-package/foo"; import bar = require("some-package/bar"); |
Code : | Sélectionner tout |
1 2 | import * as foo from "some-package/foo"; var bar = require("some-package/bar"); |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | { "name": "some-package", "version": "0.0.1", "exports": { "./foo": { "import": "./esm/foo-from-import.mjs", "require": "./cjs/foo-from-require.cjs" }, "./bar": { "import": "./esm/bar-from-import.mjs", "require": "./cjs/bar-from-require.cjs" } } } |
Attributs et assertions d'importation vérifiés
Les attributs et assertions d'importation sont désormais vérifiés par rapport au type global ImportAttributes. Cela signifie que les moteurs d'exécution peuvent maintenant décrire plus précisément les attributs d'importation.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // In some global file. interface ImportAttributes { type: "json"; } // In some other module import * as ns from "foo" with { type: "not-json" }; // ~~~~~~~~~~ // error! // // Type '{ type: "not-json"; }' is not assignable to type 'ImportAttributes'. // Types of property 'type' are incompatible. // Type '"not-json"' is not assignable to type '"json"'. |
Correction rapide pour l'ajout de paramètres manquants
TypeScript dispose désormais d'une solution rapide pour ajouter un nouveau paramètre aux fonctions qui sont appelées avec trop d'arguments.
Cela peut être utile pour faire passer un nouvel argument à travers plusieurs fonctions existantes, ce qui peut être encombrant aujourd'hui.
Changements à venir suite aux dépréciations de TypeScript 5.0
TypeScript 5.0 a rendu obsolètes les options et comportements suivants :
- target: ES3
- noImplicitUseStrict
- keyofStringsOnly
- suppressExcessPropertyErrors
- suppressImplicitAnyIndexErrors
- noStrictGenericChecks
- charset
- out
- prepend pour les projets références
- newLine implicitement spécifique à l'OS
Pour continuer à les utiliser, les développeurs utilisant TypeScript 5.0 et d'autres versions plus récentes ont dû spécifier une nouvelle option appelée ignoreDeprecations avec la valeur "5.0".
Cependant, TypScript 5.4 sera la dernière version dans laquelle ils continueront à fonctionner normalement. D'ici TypeScript 5.5 (probablement juin 2024), elles deviendront des erreurs difficiles à corriger, et le code qui les utilise devra être migré.
Changements en cours
Changements dans lib.d.ts
Les types générés pour le DOM peuvent avoir un impact sur votre base de code.
Contraintes de type conditionnel plus précises
Le code suivant n'autorise plus la deuxième déclaration de variable dans la fonction foo.
Code : | Sélectionner tout |
1 2 3 4 5 6 | type IsArray<T> = T extends any[] ? true : false; function foo<U extends object>(x: IsArray<U>) { let first: true = x; // Error let second: false = x; // Error, but previously wasn't } |
Mais ce comportement était inexact car il était trop enthousiaste. Même si la contrainte de T n'est pas assignable à Foo, cela ne signifie pas qu'elle ne sera pas instanciée avec quelque chose qui l'est. Le comportement le plus correct est donc de produire un type union pour la contrainte du type conditionnel dans les cas où il n'est pas possible de prouver que T n'étend jamais ou toujours Foo.
TypeScript 5.4 adopte ce comportement plus précis. En pratique, cela signifie que certaines instances de types conditionnels ne sont plus compatibles avec leurs branches.
Réduction plus agressive des intersections entre les variables de type et les types primitifs
TypeScript réduit maintenant les intersections entre les variables de type et les primitives de manière plus agressive, en fonction de la façon dont la contrainte de la variable de type se superpose à ces primitives.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | declare function intersect<T, U>(x: T, y: U): T & U; function foo<T extends "abc" | "def">(x: T, str: string, num: number) { // Was 'T & string', now is just 'T' let a = intersect(x, str); // Was 'T & number', now is just 'never' let b = intersect(x, num) // Was '(T & "abc") | (T & "def")', now is just 'T' let c = Math.random() < 0.5 ? intersect(x, "abc") : intersect(x, "def"); } |
Code : | Sélectionner tout |
1 2 3 4 5 6 | function a<T extends {id: string}>() { let x: `-${keyof T & string}`; // Used to error, now doesn't. x = "-id"; } |
Erreurs lorsque les importations de type seulement entrent en conflit avec les valeurs locales
Auparavant, TypeScript autorisait le code suivant sous isolatedModules si l'importation vers Something ne faisait référence qu'à un type.
Code : | Sélectionner tout |
1 2 3 | import { Something } from "./some/path"; let Something = 123; |
Code : | Sélectionner tout |
Import 'Something' conflicts with local value, so must be declared with a type-only import when 'isolatedModules' is enabled.
Code : | Sélectionner tout |
1 2 3 4 5 | import type { Something } from "./some/path"; // or import { type Something } from "./some/path"; |
Bien qu'il ne s'agisse pas d'un changement radical en soi, les développeurs peuvent avoir implicitement pris des dépendances sur les sorties d'émission de JavaScript ou de déclaration de TypeScript. Les changements suivants sont notables.
- Préserver plus souvent les noms des paramètres de type lorsqu'ils sont masqués.
- Déplacer les listes de paramètres complexes des fonctions asynchrones dans le corps du générateur de niveau inférieur.
- Ne pas supprimer l'alias de liaison dans les déclarations de fonction
- Les ImportAttributes devraient passer par les mêmes phases d'émission lorsqu'ils se trouvent dans un ImportTypeNode.
Quelles sont les prochaines étapes ?
À ce stade, TypeScript 5.4 est ce qu'on appelle "stable en termes de fonctionnalités". TypeScript 5.4 se concentrera sur les corrections de bogues, le polissage et certaines fonctionnalités d'édition à faible risque. Une version candidate sera disponible dans un peu plus d'un mois, suivie d'une version stable peu après. Si vous souhaitez planifier la sortie de cette version, n'oubliez pas de garder un œil sur notre plan d'itération qui indique les dates de sortie et bien d'autres choses encore.
Remarque : bien que la version bêta soit un excellent moyen d'essayer la prochaine version de TypeScript, vous pouvez également essayer une version de nuit pour obtenir la version la plus récente de TypeScript 5.4 jusqu'à notre candidat à la publication. Nos nightlies sont bien testées et peuvent même être testées uniquement dans votre éditeur.
Alors n'hésitez pas à essayer la version bêta ou une version de nuit dès aujourd'hui et faites-nous savoir ce que vous en pensez !
Remarque : bien que la version bêta soit un excellent moyen d'essayer la prochaine version de TypeScript, vous pouvez également essayer une version de nuit pour obtenir la version la plus récente de TypeScript 5.4 jusqu'à notre candidat à la publication. Nos nightlies sont bien testées et peuvent même être testées uniquement dans votre éditeur.
Alors n'hésitez pas à essayer la version bêta ou une version de nuit dès aujourd'hui et faites-nous savoir ce que vous en pensez !
Et vous ?
Quel est votre avis sur cette mise à jour de TypeScript ?
Voir aussi :
Cinq vérités inconfortables à propos de TypeScript selon Stefan Baumgartner, auteur de livres sur le langage de programmation
Microsoft annonce la disponibilité de TypeScript 5.3 Release Candidate, documentant le resolution-mode et ajoutant une option de préférence pour l'importation automatique des Type
TypeScript a 10 ans ! Joyeux anniversaire. À cette occasion, Microsoft fait le point. L'entreprise revient sur les doutes des premiers jours ainsi que sur l'évolution du langage