TypeScript 4.8 introduit plusieurs optimisations qui devraient accélérer les scénarios autour de --watch et --incremental, ainsi que les constructions de références de projet à l'aide de --build. Par exemple, TypeScript est désormais en mesure d'éviter de passer du temps à mettre à jour les horodatages pendant les modifications sans opération en mode --watch, ce qui accélère les reconstructions et évite de déranger les autres outils de construction qui pourraient surveiller la sortie de TypeScript. De nombreuses autres optimisations permettant de réutiliser les informations dans --build, --watch et --incremental ont également été introduites.
Quelle est l'ampleur de ces améliorations*? Eh bien, sur une base de code interne assez importante, l'équipe TypeScript a constaté des réductions de temps de l'ordre de 10*% à 25*% sur de nombreuses opérations courantes simples, avec des réductions de temps d'environ 40*% dans des scénarios sans changement. L'équipe a également constaté des résultats similaires sur la base de code TypeScript.
Erreurs lors de la comparaison des littéraux d'objet et de tableau
Dans de nombreux langages, les opérateurs comme == effectuent ce qu'on appelle l'égalité de « valeur » sur les objets. Par exemple, en Python, il est valide de vérifier si une liste est vide en vérifiant si une valeur est égale à la liste vide en utilisant ==.
Code Python : | Sélectionner tout |
1 2 | if people_at_home == []: print("that's where she lies, broken inside. </3") |
Ce n'est pas le cas en JavaScript, où == et === entre les objets et les tableaux vérifient si les deux références pointent vers la même instance. L'équipe TypeScript pense qu'il s'agit au mieux d'une première amorce pour les développeurs JavaScript, et au pire d'un bogue dans le code de production. C'est pourquoi TypeScript interdit désormais le code comme celui-ci.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 | let peopleAtHome = []; if (peopleAtHome === []) { // ~~~~~~~~~~~~~~~~~~~ // This condition will always return 'false' since JavaScript compares objects by reference, not value. console.log("that's where she lies, broken inside. </3") } |
Inférence améliorée à partir de modèles de liaison
Dans certains cas, TypeScript sélectionnera un type à partir d'un modèle de liaison pour faire de meilleures inférences.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | declare function chooseRandomly<T>(x: T, y: T): T; let [a, b, c] = chooseRandomly([42, true, "hi!"], [0, false, "bye!"]); // ^ ^ ^ // | | | // | | string // | | // | boolean // | // number |
Lorsque chooseRandomly a besoin de trouver un type pour T, il regardera principalement [42, true, "hi!"] et [0, false, "bye!"]*; mais TypeScript doit déterminer si ces deux types doivent être Array<number | boolean | string> ou le type de tuple [number, boolean, string]. Pour ce faire, il recherchera les candidats existants comme un indice pour voir s'il existe des types de tuples. Lorsque TypeScript voit le modèle de liaison [a, b, c], il crée le type [any, any, any], et ce type est sélectionné comme candidat de faible priorité pour T qui est également utilisé comme indice pour les types de [42, true, "hi!"] et [0, false, "bye!"].
Vous pouvez voir à quel point cela était bon pour chooseRandomly, mais cela a échoué dans d'autres cas. Par exemple, prenez le code suivant
Code TypeScript : | Sélectionner tout |
1 2 3 | declare function f<T>(x?: T): T; let [x, y, z] = f(); |
Le motif de liaison [x, y, z] indiquait que f devrait produire un tuple [any, any, any]*; mais f ne devrait vraiment pas changer son argument de type en fonction d'un modèle de liaison. Il ne peut pas soudainement évoquer une nouvelle valeur de type tableau en fonction de ce à quoi il est assigné, de sorte que le type de modèle de liaison a beaucoup trop d'influence sur le type produit. En plus de cela, parce que le type de modèle de liaison est plein de any, nous nous retrouvons avec x, y et z tapés comme any.
Dans TypeScript 4.8, ces modèles de liaison ne sont jamais utilisés comme candidats pour les arguments de type. Au lieu de cela, ils sont simplement consultés au cas où un paramètre nécessiterait un type plus spécifique, comme dans notre exemple chooseRandomly. Si vous devez revenir à l'ancien comportement, vous pouvez toujours fournir des arguments de type explicites.
Inférence améliorée pour les types d'inférence dans les types de chaîne de template
TypeScript a récemment introduit un moyen d'ajouter des contraintes d'extension pour déduire des variables de type dans des types conditionnels.
Code TypeScript : | Sélectionner tout |
1 2 3 4 | // Grabs the first element of a tuple if it's assignable to 'number', // and returns 'never' if it can't find one. type TryGetNumberIfFirst<T> = T extends [infer U extends number, ...unknown[]] ? U : never; |
Si ces types d'inférence apparaissent dans un type de chaîne de template et sont contraints à un type primitif, TypeScript essaiera maintenant d'analyser un type littéral.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 | // SomeNum used to be 'number'; now it's '100'. type SomeNum = "100" extends `${infer U extends number}` ? U : never; // SomeBigInt used to be 'bigint'; now it's '100n'. type SomeBigInt = "100" extends `${infer U extends bigint}` ? U : never; // SomeBool used to be 'boolean'; now it's 'true'. type SomeBool = "true" extends `${infer U extends boolean}` ? U : never; |
Cela peut maintenant mieux transmettre ce qu'une bibliothèque fera au moment de l'exécution et donner des types plus précis.
Une note à ce sujet est que lorsque TypeScript analyse ces types littéraux, il essaiera avidement d'analyser autant de ce qui ressemble au type primitif approprié; cependant, il vérifie ensuite si la réimpression de cette primitive correspond au contenu de la chaîne. En d'autres termes, TypeScript vérifie si le passage de la chaîne à la primitive et le retour correspondent. Si ce n'est pas le cas, alors il reviendra au type primitif de base.
Code TypeScript : | Sélectionner tout |
1 2 | // JustNumber is `number` here because TypeScript parses out `"1.0"`, but `String(Number("1.0"))` is `"1"` and doesn't match. type JustNumber = "1.0" extends `${infer T extends number}` ? T : never; |
Amélioration de la réduction des intersections et de la compatibilité des unions
TypeScript 4.8 apporte une série d'améliorations d'exactitude et de cohérence sous --strictNullChecks. Ces changements affectent le fonctionnement des types d'intersection et d'union et sont exploités dans la façon dont TypeScript restreint les types.
Par exemple, unknown est proche dans l'esprit du type d'union {} | nul | undefined car il accepte null, undefined et tout autre type. TypeScript le reconnaît désormais et autorise les affectations de unknown à {} | null | undefined.
Code TypeScript : | Sélectionner tout |
1 2 3 4 | function f(x: unknown, y: {} | null | undefined) { x = y; // always worked y = x; // used to error, now works } |
Un autre changement est que l'intersection de {} avec tout autre type d'objet se simplifie jusqu'à ce type d'objet. Cela signifie que nous sommes en mesure de réécrire NonNullable pour utiliser simplement une intersection avec {}, car {} & null et {} & undefined sont tout simplement jetés.
Code TypeScript : | Sélectionner tout |
1 2 | - type NonNullable<T> = T extends null | undefined ? never : T; + type NonNullable<T> = T & {}; |
Il s'agit d'une amélioration car les types d'intersection comme celui-ci peuvent être réduits et affectés, contrairement aux types conditionnels actuellement. Donc NonNullable<NonNullable<T>> se simplifie maintenant au moins en NonNullable<T>, alors qu'il ne l'était pas auparavant.
Code TypeScript : | Sélectionner tout |
1 2 3 4 | function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) { x = y; // always worked y = x; // used to error, now works } |
Ces changements ont également permis à l'équipe TypeScript d'apporter des améliorations sensibles à l'analyse des flux de contrôle et au rétrécissement des types. Par exemple, unknown est maintenant rétréci comme {} | nul | undefined dans les truthy branches branches de vérité. On dit en anglais qu'une valeur est truthy lorsqu'elle est considérée comme vraie (true) quand elle est évaluée dans un contexte booléen. Toutes les valeurs sont truthy sauf si elles sont définies comme falsy (par exemple, en JavaScript, sauf pour false, 0, -0, 0n, "", null, undefined et NaN).
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | function narrowUnknownishUnion(x: {} | null | undefined) { if (x) { x; // {} } else { x; // {} | null | undefined } } function narrowUnknown(x: unknown) { if (x) { x; // used to be 'unknown', now '{}' } else { x; // unknown } } |
Les valeurs génériques sont également réduites de la même manière. Lors de la vérification qu'une valeur n'est pas nulle ou indéfinie, TypeScript la croise maintenant avec {} (ce qui, encore une fois, revient à dire qu'elle est NonNullable). En rassemblant de nombreux changements ici, nous pouvons maintenant définir la fonction suivante sans aucune assertion de type.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | function throwIfNullable<T>(value: T): NonNullable<T> { if (value === undefined || value === null) { throw Error("Nullable value!"); } // Used to fail because 'T' was not assignable to 'NonNullable<T>'. // Now narrows to 'T & {}' and succeeds because that's just 'NonNullable<T>'. return value; } |
value est maintenant réduite à T & {}, et est maintenant identique à NonNullable<T> (donc le corps de la fonction fonctionne simplement sans syntaxe spécifique à TypeScript).
En eux-mêmes, ces changements peuvent sembler minimes, mais ils représentent des correctifs pour de nombreux cas qui ont été signalés sur plusieurs années.
Corrections de surveillance de fichiers (en particulier lors des vérifications git)
L'équipe Typescript reconnaît avoir eu un bogue de longue date où TypeScript a beaucoup de mal avec certaines modifications de fichiers dans le mode --watch et les scénarios d'éditeur. Parfois, les symptômes sont des erreurs obsolètes ou inexactes qui peuvent apparaître et nécessiter le redémarrage de tsc ou de VS Code. Ceux-ci se produisent fréquemment sur les systèmes Unix, et vous les avez peut-être vus après avoir enregistré un fichier avec vim ou échangé des branches dans git.
Cela a été causé par des hypothèses sur la façon dont Node.js gère les événements de changement de nom dans les systèmes de fichiers. Les systèmes de fichiers utilisés par Linux et macOS utilisent des inodes, et Node.js attachera des observateurs de fichiers aux inodes plutôt qu'aux chemins de fichiers. Ainsi, lorsque Node.js renvoie un objet observateur, il peut surveiller un chemin ou un inode en fonction de la plate-forme et du système de fichiers.
Pour être un peu plus efficace, TypeScript essaie de réutiliser les mêmes objets observateurs s'il détecte qu'un chemin existe toujours sur le disque. C'est là que les choses se sont mal passées, car même si un fichier existe toujours sur ce chemin, un fichier distinct peut avoir été créé et ce fichier aura un inode différent. Ainsi, TypeScript finirait par réutiliser l'objet observateur au lieu d'installer un nouvel observateur à l'emplacement d'origine, et surveillerait les modifications apportées à ce qui pourrait être un fichier totalement non pertinent. Ainsi, TypeScript 4.8 gère désormais ces cas sur les systèmes inode et installe correctement un nouvel observateur et corrige cela.
Améliorations des performances de la recherche de toutes les références
Lors de l'exécution de la recherche de toutes les références dans votre éditeur, TypeScript est désormais capable d'agir un peu plus intelligemment lorsqu'il agrège les références. Cela a réduit d'environ 20*% le temps nécessaire à TypeScript pour rechercher un identifiant largement utilisé dans sa propre base de code.
Changements substanciels
En raison de la nature des modifications du système de type, très peu de modifications peuvent être apportées sans affecter certains codes*; cependant, il y a quelques changements qui sont plus susceptibles de nécessiter l'adaptation du code existant.
Mises à jour de lib.d.ts
Alors que TypeScript s'efforce d'éviter les ruptures majeures, même de petites modifications dans les bibliothèques intégrées peuvent causer des problèmes. L'équipe ne prévoit pas de ruptures majeures à la suite des mises à jour de DOM et de lib.d.ts, mais il peut y en avoir de petites.
Les génériques sans contrainte ne peuvent plus être attribués à {}
Dans TypeScript 4.8, pour les projets avec strictNullChecks activé, TypeScript émet désormais correctement une erreur lorsqu'un paramètre de type sans contrainte est utilisé dans une position où null ou undefined ne sont pas des valeurs légales. Cela inclura tout type qui attend {}, un objet ou un type d'objet avec des propriétés entièrement facultatives.
Un exemple simple peut être vu dans ce qui suit :
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Accepts any non-null non-undefined value function bar(value: {}) { Object.keys(value); // This call throws on null/undefined at runtime. } // Unconstrained type parameter T... function foo<T>(x: T) { bar(x); // Used to be allowed, now is an error in 4.8. // ~ // error: Argument of type 'T' is not assignable to parameter of type '{}'. } foo(undefined); |
Comme démontré ci-dessus, un code comme celui-ci a un bogue potentiel - les valeurs null et undefined peuvent être indirectement transmises via ces paramètres de type sans contrainte au code qui n'est pas censé respecter ces valeurs.
Ce comportement sera également visible dans les positions de type. Un exemple serait :
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 | interface Foo<T> { x: Bar<T>; } interface Bar<T extends {}> { } |
Le code existant qui ne voulait pas gérer null et undefined peut être corrigé en propageant les contraintes appropriées.
Code TypeScript : | Sélectionner tout |
1 2 | - function foo<T>(x: T) { + function foo<T extends {}>(x: T) { |
Une autre solution consisterait à vérifier null et undefined au moment de l'exécution.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 | function foo<T>(x: T) { + if (x !== null && x !== undefined) { bar(x); + } } |
Et si vous savez que pour une raison quelconque, votre valeur générique ne peut pas être nulle ou indéfinie, vous pouvez simplement utiliser une assertion non nulle.
Code TypeScript : | Sélectionner tout |
1 2 3 4 | function foo<T>(x: T) { - bar(x); + bar(x!); } |
Les types ne peuvent pas être importés/exportés dans les fichiers JavaScript
TypeScript permettait auparavant aux fichiers JavaScript d'importer et d'exporter des entités déclarées avec un type, mais sans valeur, dans les instructions d'importation et d'exportation. Ce comportement était incorrect, car les importations et les exportations nommées pour des valeurs qui n'existent pas entraîneront une erreur d'exécution sous les modules ECMAScript. Lorsqu'un fichier JavaScript est vérifié par type sous --checkJs ou via un commentaire // @ts-check, TypeScript émet désormais une erreur.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // @ts-check // Will fail at runtime because 'SomeType' is not a value. import { someValue, SomeType } from "some-module"; /** * @type {SomeType} */ export const myValue = someValue; /** * @typedef {string | number} MyType */ // Will fail at runtime because 'MyType' is not a value. export { MyType as MyExportedType }; |
Pour référencer un type d'un autre module, vous pouvez à la place directement qualifier l'import.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 | - import { someValue, SomeType } from "some-module"; + import { someValue } from "some-module"; /** - * @type {SomeType} + * @type {import("some-module").SomeType} */ export const myValue = someValue; |
Pour exporter un type, vous pouvez simplement utiliser un commentaire /** @typedef */ dans JSDoc. Les commentaires @typedef exportent déjà automatiquement les types de leurs modules conteneurs.
Code TypeScript : | Sélectionner tout |
1 2 3 4 5 6 7 8 | /** * @typedef {string | number} MyType */ + /** + * @typedef {MyType} MyExportedType + */ - export { MyType as MyExportedType }; |
Source : Microsoft