L'opérateur satisfies
Les développeurs TypeScript sont souvent confrontés à un dilemme : ils veulent s'assurer qu'une expression correspond à un certain type, mais ils voulent également conserver le type le plus spécifique de cette expression à des fins d'inférence. Par exemple :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // Each property can be a string or an RGB tuple. const palette = { red: [255, 0, 0], green: "#00ff00", bleu: [0, 0, 255] // ^^^^ sacrebleu - we've made a typo! }; // We want to be able to use array methods on 'red'... const redComponent = palette.red.at(0); // or string methods on 'green'... const greenNormalized = palette.green.toUpperCase(); |
Remarquez que Microsoft écrit bleu, alors que nous elle aurait probablement dû écrire blue. Nous pourrions essayer de corriger la faute de frappe de bleu en utilisant une annotation de type sur palette, mais nous perdrions les informations sur chaque propriété.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | type Colors = "red" | "green" | "blue"; type RGB = [red: number, green: number, blue: number]; const palette: Record<Colors, string | RGB> = { red: [255, 0, 0], green: "#00ff00", bleu: [0, 0, 255] // ~~~~ The typo is now correctly detected }; // But we now have an undesirable error here - 'palette.red' "could" be a string. const redComponent = palette.red.at(0); |
Le nouvel opérateur satisfies nous permet de valider que le type d'une expression correspond à un certain type, sans modifier le type résultant de cette expression. Par exemple, nous pourrions utiliser satisfies pour valider que toutes les propriétés de palette sont compatibles avec string | number[] :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | type Colors = "red" | "green" | "blue"; type RGB = [red: number, green: number, blue: number]; const palette = { red: [255, 0, 0], green: "#00ff00", bleu: [0, 0, 255] // ~~~~ The typo is now caught! } satisfies Record<Colors, string | RGB>; // Both of these methods are still accessible! const redComponent = palette.red.at(0); const greenNormalized = palette.green.toUpperCase(); |
satisfies peut être utilisé pour visualiser de nombreuses erreurs possibles. Par exemple, nous pouvons nous assurer qu'un objet possède toutes les clés d'un certain type, mais pas plus :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | type Colors = "red" | "green" | "blue"; // Ensure that we have exactly the keys from 'Colors'. const favoriteColors = { "red": "yes", "green": false, "blue": "kinda", "platypus": false // ~~~~~~~~~~ error - "platypus" was never listed in 'Colors'. } satisfies Record<Colors, unknown>; // All the information about the 'red', 'green', and 'blue' properties are retained. const g: boolean = favoriteColors.green; |
Peut-être que nous ne nous soucions pas de savoir si les noms des propriétés correspondent d'une manière ou d'une autre, mais que nous nous intéressons aux types de chaque propriété. Dans ce cas, nous pouvons également nous assurer que toutes les valeurs des propriétés d'un objet sont conformes à un certain type.
[CODE]
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | type RGB = [red: number, green: number, blue: number]; const palette = { red: [255, 0, 0], green: "#00ff00", blue: [0, 0] // ~~~~~~ error! } satisfies Record<string, string | RGB>; // Information about each property is still maintained. const redComponent = palette.red.at(0); const greenNormalized = palette.green.toUpperCase(); |
Restriction des propriétés non listées avec l'opérateur in
En tant que développeurs, nous devons souvent traiter des valeurs qui ne sont pas entièrement connues au moment de l'exécution. En fait, il arrive souvent que nous ne sachions pas si des propriétés existent, que nous obtenions une réponse d'un serveur ou que nous lisions un fichier de configuration. L'opérateur in de JavaScript peut vérifier si une propriété existe sur un objet.
Auparavant, TypeScript nous permettait d'éliminer tous les types qui ne listaient pas explicitement une propriété.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | interface RGB { red: number; green: number; blue: number; } interface HSV { hue: number; saturation: number; value: number; } function setColor(color: RGB | HSV) { if ("hue" in color) { // 'color' now has the type HSV } // ... } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | function tryGetPackageName(context) { const packageJSON = context.packageJSON; // Check to see if we have an object. if (packageJSON && typeof packageJSON === "object") { // Check to see if it has a string name property. if ("name" in packageJSON && typeof packageJSON.name === "string") { return packageJSON.name; } } return undefined; } |
La réécriture en TypeScript classique serait juste une question de définition et d'utilisation d'un type pour le contexte ; cependant, le choix d'un type sûr comme [C]unknown[C] pour la propriété packageJSON causerait des problèmes dans les anciennes versions de TypeScript.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | interface Context { packageJSON: unknown; } function tryGetPackageName(context: Context) { const packageJSON = context.packageJSON; // Check to see if we have an object. if (packageJSON && typeof packageJSON === "object") { // Check to see if it has a string name property. if ("name" in packageJSON && typeof packageJSON.name === "string") { // ~~~~ // error! Property 'name' does not exist on type 'object. return packageJSON.name; // ~~~~ // error! Property 'name' does not exist on type 'object. } } return undefined; } |
En effet, alors que le type de packageJSON est passé d'inconnu à objet, l'opérateur in est strictement limité aux types qui définissent réellement la propriété vérifiée. Par conséquent, le type de packageJSON est resté objet.
TypeScript 4.9 rend l'opérateur in un peu plus puissant lorsqu'il s'agit de restreindre les types qui ne répertorient pas du tout la propriété. Au lieu de les laisser tels quels, le langage intersectera leurs types avec Record<"property-key-being-checked", unknown>. Ainsi, dans notre exemple, le type de packageJSON sera réduit de unknown à objet à objet & Record<"name", inconnu>. Cela nous permet d'accéder directement à packageJSON.name et de le réduire indépendamment.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | interface Context { packageJSON: unknown; } function tryGetPackageName(context: Context): string | undefined { const packageJSON = context.packageJSON; // Check to see if we have an object. if (packageJSON && typeof packageJSON === "object") { // Check to see if it has a string name property. if ("name" in packageJSON && typeof packageJSON.name === "string") { // Just works! return packageJSON.name; } } return undefined; } |
TypeScript 4.9 renforce également quelques contrôles sur la façon dont in est utilisé, en s'assurant que le côté gauche est assignable au type string | number | symbol, et que le côté droit est assignable à object. Cela permet de vérifier que nous utilisons des clés de propriété valides et que nous ne vérifions pas accidentellement des primitives.
Accesseurs automatiques dans les classes
TypeScript 4.9 prend en charge une fonctionnalité à venir dans ECMAScript appelée auto-accesseurs. Les auto-accesseurs sont déclarés comme les propriétés des classes, sauf qu'ils sont déclarés avec le mot-clé accessor.
Code : | Sélectionner tout |
1 2 3 4 5 6 | class Person { accessor name: string; constructor(name: string) { this.name = name; } |
En dessous, ces auto-accesseurs se "désagrègent" en un accesseur get et set avec une propriété privée inaccessible.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | class Person { #__name: string; get name() { return this.#__name; } set name(value: string) { this.#__name = name; } constructor(name: string) { this.name = name; } } } |
Contrôles d'égalité sur NaN
L'une des principales difficultés rencontrées par les développeurs JavaScript est la vérification de la valeur NaN à l'aide des opérateurs d'égalité intégrés. Pour rappel, NaN est une valeur numérique spéciale qui signifie "Not a Number" (pas un nombre). Rien n'est jamais égal à NaN - même NaN !
Code : | Sélectionner tout |
1 2 3 4 5 | console.log(NaN == 0) // false console.log(NaN === 0) // false console.log(NaN == NaN) // false console.log(NaN === NaN) // false |
Mais au moins symétriquement, tout est toujours non égal à NaN.
Code : | Sélectionner tout |
1 2 3 4 5 | console.log(NaN != 0) // true console.log(NaN !== 0) // true console.log(NaN != NaN) // true console.log(NaN !== NaN) // true |
Techniquement, ce n'est pas un problème spécifique à JavaScript, puisque n'importe quel langage qui contient des flottants IEEE-754 a le même comportement ; mais le principal type numérique de JavaScript est un nombre à virgule flottante, et l'analyse des nombres en JavaScript peut souvent aboutir à NaN. A son tour, la vérification contre les NaN finit par être assez commune, et la manière correcte de le faire est d'utiliser Number.isNaN - mais comme nous l'avons mentionné, beaucoup de gens finissent par vérifier avec someValue === NaN à la place.
TypeScript se trompe maintenant sur les comparaisons directes avec NaN, et suggère d'utiliser une variation de Number.isNaN à la place.
Code : | Sélectionner tout |
1 2 3 4 5 6 | function validate(someValue: number) { return someValue !== NaN; // ~~~~~~~~~~~~~~~~~ // error: This condition will always return 'true'. // Did you mean '!Number.isNaN(someValue)'? } |
Ce changement devrait aider à détecter les erreurs de débutants, de la même manière que TypeScript émet actuellement des erreurs sur les comparaisons avec les littéraux d'objets et de tableaux.
La surveillance des fichiers utilise maintenant les événements du système de fichiers
Dans les versions antérieures, TypeScript s'appuyait fortement sur l'interrogation pour suivre les fichiers individuels. L'utilisation d'une stratégie d'interrogation signifiait la vérification périodique de l'état d'un fichier pour les mises à jour. Sur Node.js, fs.watchFile est le moyen intégré d'obtenir un observateur de fichiers par échantillonnage. Bien que l'interrogation ait tendance à être plus prévisible sur les plateformes et les systèmes de fichiers, cela signifie que le CPU doit être interrompu périodiquement et vérifier les mises à jour du fichier, même si rien n'a changé. Pour quelques dizaines de fichiers, cela peut ne pas être perceptible, mais sur un projet plus important avec beaucoup de fichiers - ou beaucoup de fichiers dans les node_modules - cela peut devenir un monstre de ressources.
La plupart des plateformes modernes utilisées fournissent des facilités et des APIs comme CreateIoCompletionPort, kqueue, epoll et inotify. Node.js fait abstraction de tout cela en fournissant fs.watch. Les événements du système de fichiers fonctionnent généralement très bien, mais il y a beaucoup d'inconvénients à les utiliser et, par conséquent, à utiliser l'API fs.watch. Un observateur doit faire attention à prendre en compte la surveillance des inodes, l'indisponibilité de certains systèmes de fichiers (par exemple les systèmes de fichiers en réseau), si la surveillance récursive des fichiers est disponible, si les renommages de répertoires déclenchent des événements, et même l'épuisement des observateurs de fichiers ! En d'autres termes, ce n'est pas tout à fait un repas gratuit, surtout si vous recherchez quelque chose de multiplateforme.
Par conséquent, Microsoft a choisi par défaut le plus petit dénominateur commun : l'interrogation. Pas toujours, mais la plupart du temps.
Au fil du temps, Microsoft a fourni les moyens de choisir d'autres stratégies de surveillance des fichiers. « Cela nous a permis d'obtenir un retour d'information et de renforcer notre implémentation de file-watching contre la plupart de ces problèmes spécifiques à la plateforme », déclare l'équipe TypeScript. Comme TypeScript a dû s'adapter à des bases de code plus importantes, et s'est amélioré dans ce domaine, nous avons estimé que le passage aux événements du système de fichiers par défaut serait un investissement rentable.
Dans TypeScript 4.9, la surveillance des fichiers est alimentée par les événements du système de fichiers par défaut, ne revenant à l'interrogation que si Microsoft ne parvenons pas à mettre en place des surveillances basées sur des événements. Pour la plupart des développeurs, cela devrait fournir une expérience beaucoup moins gourmande en ressources lors de l'exécution en mode --watch, ou lors de l'exécution avec un éditeur alimenté par TypeScript comme Visual Studio ou VS Code.
La façon dont la surveillance des fichiers fonctionne peut toujours être configurée par le biais de variables d'environnement et d'options de surveillance, et certains éditeurs comme VS Code peuvent prendre en charge les options de surveillance de façon indépendante. Les développeurs utilisant des configurations plus exotiques où le code source réside sur un système de fichiers en réseau (comme NFS et SMB) peuvent avoir besoin de revenir à l'ancien comportement ; bien que si un serveur a une puissance de traitement raisonnable, il pourrait être préférable d'activer SSH et d'exécuter TypeScript à distance afin qu'il ait un accès direct aux fichiers locaux. VS Code dispose de nombreuses extensions à distance pour faciliter cette opération.
Commandes "Remove Unused Imports" et "Sort Imports" pour les éditeurs
Auparavant, TypeScript ne prenait en charge que deux commandes d'éditeur pour gérer les importations. Pour nos exemples, prenons le code suivant :
Code : | Sélectionner tout |
1 2 3 4 | import { Zebra, Moose, HoneyBadger } from "./zoo"; import { foo, bar } from "./helper"; let x: Moose | HoneyBadger = foo(); |
Code : | Sélectionner tout |
1 2 3 4 | import { foo } from "./helper"; import { HoneyBadger, Moose } from "./zoo"; let x: Moose | HoneyBadger = foo(); |
Code : | Sélectionner tout |
1 2 3 4 | import { bar, foo } from "./helper"; import { HoneyBadger, Moose, Zebra } from "./zoo"; let x: Moose | HoneyBadger = foo(); |
TypeScript 4.9 ajoute l'autre moitié, et fournit maintenant "Remove Unused Imports". TypeScript va maintenant supprimer les noms d'importation inutilisés et les déclarations, mais autrement laissera l'ordre relatif seul.
Code : | Sélectionner tout |
1 2 3 4 | import { Moose, HoneyBadger } from "./zoo"; import { foo } from "./helper"; let x: Moose | HoneyBadger = foo(); |
Améliorations des performances
TypeScript a quelques petites, mais notables, améliorations de performance
Tout d'abord, la fonction forEachChild de TypeScript a été réécrite pour utiliser une consultation de table de fonctions au lieu d'une instruction switch sur tous les nœuds syntaxiques. forEachChild est un cheval de bataille pour traverser les nœuds syntaxiques dans le compilateur, et est fortement utilisé dans l'étape de liaison de notre compilateur, avec des parties du service de langue. Le remaniement de forEachChild a permis de réduire jusqu'à 20 % le temps passé dans notre phase de liaison et dans les opérations du service linguistique.
Après avoir découvert ce gain de performance pour forEachChild, nous l'avons essayé sur visitEachChild, une fonction que nous utilisons pour transformer les nœuds dans le compilateur et le service de langue. La même refactorisation a permis de réduire jusqu'à 3 % le temps passé à générer les résultats du projet. L'exploration initiale de forEachChild a été inspirée par un article de blog d'Artemis Everfree.Enfin, la façon dont TypeScript préserve les informations sur un type dans la branche vraie d'un type conditionnel a été optimisée. Dans un type comme
Code : | Sélectionner tout |
1 2 3 4 5 | interface Zoo<T extends Animal> { // ... } type MakeZoo<A> = A extends Animal ? Zoo<A> : never; |
TypeScript doit « se souvenir » que A doit également être un animal lorsqu'il vérifie si Zoo<A> est valide. Cela se fait essentiellement en créant un type spécial qui contient l'intersection de A avec Animal ; cependant, TypeScript le faisait auparavant de manière empressée, ce qui n'est pas toujours nécessaire. En outre, un code défectueux dans le vérificateur de type a empêché ces types spéciaux d'être simplifiés.
TypeScript reporte maintenant l'intersection de ces types jusqu'à ce que cela soit nécessaire. Pour les bases de code avec une utilisation intensive des types conditionnels, vous pourriez assister à des accélérations significatives avec TypeScript, mais dans notre suite de tests de performance, nous avons vu une réduction plus modeste de 3 % du temps de vérification de type.
Source : Microsoft
Et vous ?
Que pensez-vous de TypeScript ?
Avez-vous déjà utilisé TypeScript ? Pour des projets personnels ou en entreprise ?
Si vous ne l'avez pas encore utilisé, prévoyez-vous de le faire ? Pourquoi ?
Quelles améliorations vous intéressent le plus ?
Voir aussi :
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
TypeScript 4.8 bêta apporte des améliorations de performances à --watch et --build, mais empêche l'importation/l'exportation des types dans les fichiers JavaScript
TypeScript 4.8 RC permet d'exclure des fichiers spécifiques des importations automatiques et améliore l'inférence à partir de modèles de liaison
TypeScript 4.9 Beta est disponible et apporte la restriction des propriétés non listées avec l'opérateur in, ainsi que la vérification de l'égalité sur NaN