Prédicats de type inférés
L'analyse du flux de contrôle de TypeScript fait un excellent travail de suivi de la façon dont le type d'une variable change au fur et à mesure qu'elle se déplace dans votre code :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | interface Bird { commonName: string; scientificName: string; sing(): void; } // Maps country names -> national bird. // Not all nations have official birds (looking at you, Canada!) declare const nationalBirds: Map<string, Bird>; function makeNationalBirdCall(country: string) { const bird = nationalBirds.get(country); // bird has a declared type of Bird | undefined if (bird) { bird.sing(); // bird has type Bird inside the if statement } else { // bird has type undefined here. } } |
Dans le passé, ce type de raffinement de type était plus difficile à appliquer aux tableaux. Cela aurait été une erreur dans toutes les versions précédentes de TypeScript :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | function makeBirdCalls(countries: string[]) { // birds: (Bird | undefined)[] const birds = countries .map(country => nationalBirds.get(country)) .filter(bird => bird !== undefined); for (const bird of birds) { bird.sing(); // error: 'bird' is possibly 'undefined'. } } |
Avec TypeScript 5.5, le vérificateur de type n'a rien à redire à ce code :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | function makeBirdCalls(countries: string[]) { // birds: Bird[] const birds = countries .map(country => nationalBirds.get(country)) .filter(bird => bird !== undefined); for (const bird of birds) { bird.sing(); // ok! } } |
Cela fonctionne parce que TypeScript déduit maintenant un prédicat de type pour la fonction de filtrage. Vous pouvez voir plus clairement ce qui se passe en la transformant en une fonction indépendante :
Code : | Sélectionner tout |
1 2 3 4 | // function isBirdReal(bird: Bird | undefined): bird is Bird function isBirdReal(bird: Bird | undefined) { return bird !== undefined; } |
TypeScript déduira qu'une fonction renvoie un prédicat de type si ces conditions sont remplies :
- La fonction n'a pas d'annotation explicite de type de retour ou de prédicat de type.
- La fonction a un seul énoncé de retour et aucun retour implicite.
- La fonction ne modifie pas son paramètre.
- La fonction renvoie une expression booléenne liée à un raffinement du paramètre.
En règle générale, tout se passe comme prévu. Voici quelques autres exemples de prédicats de types déduits :
Code : | Sélectionner tout |
1 2 3 4 5 | // const isNumber: (x: unknown) => x is number const isNumber = (x: unknown) => typeof x === 'number'; // const isNonNullish: <T>(x: T) => x is NonNullable<T> const isNonNullish = <T,>(x: T) => x != null; |
Les prédicats de type ont une sémantique « si et seulement si ». Si une fonction renvoie x is T, cela signifie que :
- Si la fonction renvoie un résultat vrai, x est de type T.
- Si la fonction renvoie faux, x n'est pas de type T.
Si vous vous attendez à ce qu'un prédicat de type soit déduit mais qu'il ne l'est pas, vous risquez de vous heurter à la deuxième règle. Ce problème se pose souvent dans le cadre des vérifications de « véracité » :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | function getClassroomAverage(students: string[], allScores: Map<string, number>) { const studentScores = students .map(student => allScores.get(student)) .filter(score => !!score); return studentScores.reduce((a, b) => a + b) / studentScores.length; // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ // error: Object is possibly 'undefined'. } |
Comme dans le premier exemple, il est préférable de filtrer explicitement les valeurs indéfinies :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 | function getClassroomAverage(students: string[], allScores: Map<string, number>) { const studentScores = students .map(student => allScores.get(student)) .filter(score => score !== undefined); return studentScores.reduce((a, b) => a + b) / studentScores.length; // ok! } |
Les prédicats de type explicites continuent à fonctionner exactement comme avant. TypeScript ne vérifiera pas s'il déduit le même prédicat de type. Les prédicats de type explicites (« is ») ne sont pas plus sûrs qu'une assertion de type (« as »).
Il est possible que cette fonctionnalité casse du code existant si TypeScript déduit maintenant un type plus précis que ce que vous souhaitez. Par exemple :
Code : | Sélectionner tout |
1 2 3 4 5 | // Previously, nums: (number | null)[] // Now, nums: number[] const nums = [1, 2, 3, null, 5].filter(x => x !== null); nums.push(null); // ok in TS 5.4, error in TS 5.5 |
Code : | Sélectionner tout |
1 2 | const nums: (number | null)[] = [1, 2, 3, null, 5].filter(x => x !== null); nums.push(null); // ok in all versions |
Réduction du flux de contrôle pour les accès indexés constants
TypeScript est désormais capable de réduire les expressions de la forme obj[key] lorsque obj et key sont effectivement constants.
Code : | Sélectionner tout |
1 2 3 4 5 6 | function f1(obj: Record<string, unknown>, key: string) { if (typeof obj[key] === "string") { // Now okay, previously was error obj[key].toUpperCase(); } } |
Importations de types dans JSDoc
Aujourd'hui, si vous voulez importer quelque chose uniquement pour vérifier le type dans un fichier JavaScript, c'est encombrant. Les développeurs JavaScript ne peuvent pas simplement importer un type nommé SomeType s'il n'est pas présent au moment de l'exécution.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // ./some-module.d.ts export interface SomeType { // ... } // ./index.js import { SomeType } from "./some-module"; // runtime error! /** * @param {SomeType} myValue */ function doSomething(myValue) { // ... } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | import * as someModule from "./some-module"; /** * @param {someModule.SomeType} myValue */ function doSomething(myValue) { // ... } |
Pour éviter cela, les développeurs devaient généralement utiliser des types import(...) dans les commentaires JSDoc.
Code : | Sélectionner tout |
1 2 3 4 5 6 | /** * @param {import("./some-module").SomeType} myValue */ function doSomething(myValue) { // ... } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | /** * @typedef {import("./some-module").SomeType} SomeType */ /** * @param {SomeType} myValue */ function doSomething(myValue) { // ... } |
C'est pourquoi TypeScript prend désormais en charge une nouvelle balise de commentaire @import qui a la même syntaxe que les importations ECMAScript.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | /** @import { SomeType } from "some-module" */ /** * @param {SomeType} myValue */ function doSomething(myValue) { // ... } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | /** @import * as someModule from "some-module" */ /** * @param {someModule.SomeType} myValue */ function doSomething(myValue) { // ... } |
Vérification syntaxique des expressions régulières
Jusqu'à présent, TypeScript a généralement ignoré la plupart des expressions régulières dans le code. En effet, les expressions régulières ont techniquement une grammaire extensible et TypeScript n'a jamais fait d'effort pour compiler les expressions régulières dans les versions antérieures de JavaScript. Néanmoins, cela signifiait que de nombreux problèmes courants n'étaient pas découverts dans les expressions régulières et qu'ils se transformaient en erreurs au moment de l'exécution ou échouaient silencieusement.
Mais TypeScript effectue désormais un contrôle syntaxique de base sur les expressions régulières !
Code : | Sélectionner tout |
1 2 3 4 | let myRegex = /@robot(\s+(please|immediately)))? do some task/; // ~ // error! // Unexpected ')'. Did you mean to escape it with backslash? |
Code : | Sélectionner tout |
1 2 3 4 5 | let myRegex = /@typedef \{import\((.+)\)\.([a-zA-Z_]+)\} \3/u; // ~ // error! // This backreference refers to a group that does not exist. // There are only 2 capturing groups in this regular expression. |
Code : | Sélectionner tout |
1 2 3 4 | let myRegex = /@typedef \{import\((?<importPath>.+)\)\.(?<importedEntity>[a-zA-Z_]+)\} \k<namedImport>/; // ~~~~~~~~~~~ // error! // There is no capturing group named 'namedImport' in this regular expression. |
Code : | Sélectionner tout |
1 2 3 4 | let myRegex = /@typedef \{import\((?<importPath>.+)\)\.(?<importedEntity>[a-zA-Z_]+)\} \k<importedEntity>/; // ~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~ // error! // Named capturing groups are only available when targeting 'ES2018' or later. |
Notez que la prise en charge des expressions régulières par TypeScript est limitée aux expressions régulières littérales. Si vous essayez d'appeler new RegExp avec une chaîne littérale, TypeScript ne vérifiera pas la chaîne fournie.
Déclarations isolées
Les fichiers de déclaration (alias fichiers .d.ts) décrivent la forme des bibliothèques et modules existants en TypeScript. Cette description légère comprend les signatures de type de la bibliothèque et exclut les détails d'implémentation tels que les corps des fonctions. Ils sont publiés afin que TypeScript puisse vérifier efficacement votre utilisation d'une bibliothèque sans avoir besoin d'analyser la bibliothèque elle-même. Bien qu'il soit possible d'écrire à la main des fichiers de déclaration, si vous écrivez du code typé, il est beaucoup plus sûr et plus simple de laisser TypeScript les générer automatiquement à partir des fichiers source en utilisant --declaration.
Le compilateur TypeScript et ses API ont toujours été chargés de générer les fichiers de déclaration ; cependant, dans certains cas d'utilisation, il est préférable d'utiliser d'autres outils, ou le processus de construction traditionnel n'est pas adapté.
- Cas d'utilisation : Outils d'émission de déclarations plus rapides
Imaginez que vous souhaitiez créer un outil plus rapide pour générer des fichiers de déclaration, peut-être dans le cadre d'un service de publication ou d'un nouveau bundler. S'il existe un écosystème florissant d'outils ultra-rapides capables de transformer TypeScript en JavaScript, il n'en va pas de même pour la transformation de TypeScript en fichiers de déclaration. La raison en est que l'inférence de TypeScript nous permet d'écrire du code sans déclarer explicitement les types, ce qui signifie que l'émission de déclarations peut être complexe.
Bien que cette inférence soit importante pour le développeur, cela signifie que les outils qui souhaitent générer des fichiers de déclaration devraient reproduire certaines parties du vérificateur de type, notamment l'inférence et la capacité à résoudre les spécificateurs de modules pour suivre les importations. - Cas d'utilisation : Déclaration parallèle Emit et vérification parallèle
Imaginez que vous ayez une monorepo contenant de nombreux projets et un processeur multicœur qui souhaiterait vous aider à vérifier votre code plus rapidement. Ne serait-ce pas génial si nous pouvions vérifier tous ces projets en même temps en exécutant chaque projet sur un cœur différent ?
Malheureusement, on n'a pas la liberté de faire tout le travail en parallèle. La raison est qu'on doit construire ces projets dans l'ordre des dépendances, parce que chaque projet vérifie les fichiers de déclaration de ses dépendances. On doit donc construire la dépendance en premier pour générer les fichiers de déclaration. La fonction de références de projet de TypeScript fonctionne de la même manière, en construisant l'ensemble des projets dans l'ordre de dépendance « topologique ».
Comment pourrait-on améliorer cela ? Eh bien, si un outil rapide pouvait générer tous ces fichiers de déclaration pour le noyau en parallèle, TypeScript pourrait immédiatement suivre en vérifiant le type du noyau, du frontend et du backend également en parallèle. - Solution : Types explicites !
L'exigence commune aux deux cas d'utilisation est qu'on a besoin d'un vérificateur de types pour générer des fichiers de déclaration. C'est beaucoup demander à la communauté des outils.
Cependant, pour les développeurs qui recherchent un temps d'itération rapide et des constructions entièrement parallèles, il y a une autre façon de penser à ce problème. Un fichier de déclaration ne requiert que les types de l'API publique d'un module - en d'autres termes, les types des éléments exportés. Si, ce qui est controversé, les développeurs sont prêts à écrire explicitement les types des choses qu'ils exportent, les outils pourraient générer des fichiers de déclaration sans avoir besoin de regarder l'implémentation du module - et sans réimplémenter un vérificateur de type complet.
C'est là que la nouvelle option --isolatedDeclarations entre en jeu. --isolatedDeclarations signale les erreurs lorsqu'un module ne peut pas être transformé de manière fiable sans vérificateur de type. Plus simplement, elle fait en sorte que TypeScript signale des erreurs si vous avez un fichier qui n'est pas suffisamment annoté sur ses exportations. - Pourquoi les erreurs sont-elles souhaitables ?
Parce que cela signifie que TypeScript peut- Dire d'emblée si d'autres outils auront des problèmes avec la génération de fichiers de déclaration
- Fournir une correction rapide pour aider à ajouter ces annotations manquantes.
Ce mode ne nécessite pas d'annotations partout. Pour les locals, elles peuvent être ignorées, puisqu'elles n'affectent pas l'API publique.
Utilisation de isolatedDeclarations
isolatedDeclarations nécessite que les drapeaux declaration ou composite soient également activés.
Notez qu'isolatedDeclarations ne modifie pas la façon dont TypeScript exécute emit - seulement la façon dont il rapporte les erreurs. Il est important de noter que, comme pour isolatedModules, l'activation de cette fonctionnalité dans TypeScript n'apportera pas immédiatement les avantages potentiels discutés ici. Soyez donc patients et attendez avec impatience les développements futurs dans ce domaine. En gardant les auteurs d'outils à l'esprit, nous devrions également reconnaître qu'aujourd'hui, toutes les déclarations emit de TypeScript ne peuvent pas être facilement reproduites par d'autres outils souhaitant s'en servir comme guide.
Il est utile de rappeler que isolatedDeclarations devrait être adopté au cas par cas. Il y a une certaine ergonomie pour le développeur qui est perdue en utilisant isolatedDeclarations, et donc ce n'est peut-être pas le bon choix si votre configuration n'exploite pas les deux scénarios mentionnés plus haut. Pour les autres, le travail sur isolatedDeclarations a déjà permis de découvrir de nombreuses optimisations et opportunités pour débloquer différentes stratégies de construction parallèle. En attendant, si vous êtes prêt à faire des compromis, isolatedDeclarations peut être un outil puissant pour accélérer votre processus de construction une fois que l'outil externe sera disponible.
La variable modèle ${configDir} pour les fichiers de configuration
Il est courant dans de nombreuses bases de code de réutiliser un fichier tsconfig.json partagé qui sert de « base » à d'autres fichiers de configuration. Pour ce faire, on utilise le champ extends dans un fichier tsconfig.json.
Code : | Sélectionner tout |
1 2 3 4 5 6 | { "extends": "../../tsconfig.base.json", "compilerOptions": { "outDir": "./dist" } } |
Code : | Sélectionner tout |
1 2 3 4 5 6 | { "compilerOptions": { "typeRoots": [ "./node_modules/@types" "./custom-types" ], "outDir": "dist" } } |
- sortir dans un répertoire dist relatif au tsconfig.json dérivé, et
- avoir un répertoire custom-types relatif au tsconfig.json dérivé,
alors cela ne fonctionnerait pas. Les chemins de typeRoots seraient relatifs à l'emplacement du fichier partagé tsconfig.base.json, et non au projet qui l'étend. Chaque projet qui étend ce fichier partagé devrait déclarer ses propres outDir et typeRoots avec un contenu identique. Cela pourrait être frustrant et difficile à synchroniser entre les projets, et bien que l'exemple ci-dessus utilise typeRoots, il s'agit d'un problème courant pour les chemins et autres options.
Pour résoudre ce problème, TypeScript 5.5 introduit une nouvelle variable modèle ${configDir}. Lorsque ${configDir} est écrit dans certains champs de chemin d'un fichier tsconfig.json ou jsconfig.json, cette variable est remplacée par le répertoire contenant le fichier de configuration dans une compilation donnée. Cela signifie que le fichier tsconfig.base.json ci-dessus pourrait être réécrit comme suit :
Code : | Sélectionner tout |
1 2 3 4 5 6 | { "compilerOptions": { "typeRoots": [ "${configDir}/node_modules/@types" "${configDir}/custom-types" ], "outDir": "${configDir}/dist" } } |
Si vous avez l'intention de rendre un fichier tsconfig.json extensible, considérez si un ./ devrait plutôt être écrit avec ${configDir}.
TypeScript 5.5 apporte d'autres nouveautés, comme :
- Consultation des dépendances package.json pour la génération de fichiers de déclaration
- Amélioration de la fiabilité de l'éditeur et du mode veille
- Optimisation des performances et de la taille
- Modules ECMAScript de consommation d'API plus faciles à utiliser
- API transpileDeclaration
Des changements de comportements sont également notables et des fonctionnnalités obsolètes sont désactivées dans TypeScript 5.5.
Quelles sont les prochaines étapes ?
Microsoft :
À ce stade, TypeScript 5.5 est ce que nous appelons « feature-stable ». TypeScript 5.5 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 nocturne pour obtenir la version la plus récente de TypeScript 5.5 jusqu'à notre candidat à la publication. Nos nightlies sont bien testées et peuvent même être testées uniquement dans votre éditeur.
À ce stade, TypeScript 5.5 est ce que nous appelons « feature-stable ». TypeScript 5.5 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 nocturne pour obtenir la version la plus récente de TypeScript 5.5 jusqu'à notre candidat à la publication. Nos nightlies sont bien testées et peuvent même être testées uniquement dans votre éditeur.
Et vous ?
Quel est votre avis sur le sujet ?
Voir aussi :
Microsoft annonce la disponibilité de TypeScript 5.4. Cette version apporte un nouveau support d'auto-importation pour les importations de sous-chemins
Microsoft annonce la disponibilité de la version candidate de TypeScript 5.4. Cette version corrige des bogues critiques et contient des changements notables de comportement