Code : | Sélectionner tout |
npm install -D typescript@beta
- Utilisation des déclarations et gestion explicite des ressources
- Métadonnées des décorateurs
- Éléments "Tuple" nommés et anonymes
- Utilisation plus facile des méthodes pour les Unions de tableaux
- Compléments de virgule pour les membres d'objets
- Refonte des variables en ligne
- Changements de rupture et corrections d'erreurs
using des déclarations et de la gestion explicite des ressources
TypeScript 5.2 prend en charge la prochaine fonctionnalité de gestion explicite des ressources (Explicit Resource Management) de l'ECMAScript. Explorons quelques-unes des motivations et comprenons ce que cette fonctionnalité nous apporte.
Il est fréquent de devoir effectuer une sorte de "nettoyage" après la création d'un objet. Par exemple, vous pouvez avoir besoin de fermer des connexions réseau, de supprimer des fichiers temporaires ou simplement de libérer de la mémoire.
Imaginons une fonction qui crée un fichier temporaire, y lit et y écrit pour diverses opérations, puis le ferme et le supprime.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 | import * as fs from "fs"; export function doSomeWork() { const path = ".some_temp_file"; const file = fs.openSync(path, "w+"); // use file... // Close the file and delete it. fs.closeSync(file); fs.unlinkSync(path); } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | export function doSomeWork() { const path = ".some_temp_file"; const file = fs.openSync(path, "w+"); // use file... if (someCondition()) { // do some more work... // Close the file and delete it. fs.closeSync(file); fs.unlinkSync(path); return; } // Close the file and delete it. fs.closeSync(file); fs.unlinkSync(path); } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | export function doSomeWork() { const path = ".some_temp_file"; const file = fs.openSync(path, "w+"); try { // use file... if (someCondition()) { // do some more work... return; } } finally { // Close the file and delete it. fs.closeSync(file); fs.unlinkSync(path); } } |
Cela commence par l'ajout d'un nouveau Symbol intégré appelé Symbol.dispose, et nous pouvons créer des objets avec des méthodes nommées par Symbol.dispose. Pour plus de commodité, TypeScript définit un nouveau type global appelé Disposable qui décrit ces objets.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | class TempFile implements Disposable { #path: string; #handle: number; constructor(path: string) { this.#path = path; this.#handle = fs.openSync(path, "w+"); } // other methods [Symbol.dispose]() { // Close the file and delete it. fs.closeSync(this.#handle); fs.unlinkSync(this.#path); } } |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | export function doSomeWork() { const file = new TempFile(".some_temp_file"); try { // ... } finally { file[Symbol.dispose](); } } |
Cela nous amène à la première étoile de la fonctionnalité : les déclarations using ! using est un nouveau mot-clé qui nous permet de déclarer de nouvelles liaisons fixes, un peu comme const. La principale différence est que les variables déclarées avec using voient leur méthode Symbol.dispose appelée à la fin de la portée !
Nous aurions donc pu simplement écrire notre code comme suit :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 | export function doSomeWork() { using file = new TempFile(".some_temp_file"); // use file... if (someCondition()) { // do some more work... return; } } |
Vous connaissez peut-être les déclarations en C#, les instructions en Python ou les déclarations try-with-resource en Java. Ces déclarations sont toutes similaires au nouveau mot-clé using de JavaScript et fournissent un moyen explicite similaire d'effectuer un "nettoyage" d'un objet à la fin d'un champ d'application.
Les déclarations using effectuent ce nettoyage à la toute fin de la portée qu'elles contiennent ou juste avant un "retour anticipé" tel qu'un return ou un thrown error. Elles se débarrassent également de l'objet dans l'ordre premier entré-dernier sorti, comme une pile.
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 | function loggy(id: string): Disposable { console.log(`Creating ${id}`); return { [Symbol.dispose]() { console.log(`Disposing ${id}`); } } } function func() { using a = loggy("a"); using b = loggy("b"); { using c = loggy("c"); using d = loggy("d"); } using e = loggy("e"); return; // Unreachable. // Never created, never disposed. using f = loggy("f"); } f(); // Creating a // Creating b // Creating c // Creating d // Disposing d // Disposing c // Creating e // Disposing e // Disposing b // Disposing a |
Mais que se passe-t-il si la logique avant et pendant l'élimination génère une erreur ? Pour ces cas, SuppressedError a été introduit comme nouveau sous-type de Error. Il comporte une propriété suppressed qui contient la dernière erreur levée, et une propriété error pour l'erreur la plus récente.
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 | class ErrorA extends Error { name = "ErrorA"; } class ErrorB extends Error { name = "ErrorB"; } function throwy(id: string) { return { [Symbol.dispose]() { throw new ErrorA(`Error from ${id}`); } }; } function func() { using a = throwy("a"); throw new ErrorB("oops!") } try { func(); } catch (e: any) { console.log(e.name); // SuppressedError console.log(e.message); // An error was suppressed during disposal. console.log(e.error.name); // ErrorA console.log(e.error.message); // Error from a console.log(e.suppressed.name); // ErrorB console.log(e.suppressed.message); // oops! } |
Vous avez peut-être remarqué que nous utilisons des méthodes synchrones dans ces exemples. Cependant, une grande partie de l'élimination des ressources implique des opérations asynchrones, et nous devons attendre qu'elles soient terminées avant de continuer à exécuter tout autre code.
C'est pourquoi il existe également un nouveau Symbol.asyncDispose, qui nous amène à la prochaine vedette du spectacle — les déclarations await using. Celles-ci sont similaires aux déclarations d'utilisation, mais la clé est qu'elles recherchent la disposition qui doit await. Elles utilisent une méthode différente nommée Symbol.asyncDispose, bien qu'elles puissent également opérer sur tout ce qui possède un Symbol.dispose. Par commodité, TypeScript introduit également un type global appelé AsyncDisposable qui décrit tout objet doté d'une méthode d'élimination asynchrone.
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 37 38 39 40 41 | async function doWork() { // Do fake work for half a second. await new Promise(resolve => setTimeout(resolve, 500)); } function loggy(id: string): AsyncDisposable { console.log(`Constructing ${id}`); return { async [Symbol.asyncDispose]() { console.log(`Disposing (async) ${id}`); await doWork(); }, } } async function func() { await using a = loggy("a"); await using b = loggy("b"); { await using c = loggy("c"); await using d = loggy("d"); } await using e = loggy("e"); return; // Unreachable. // Never created, never disposed. await using f = loggy("f"); } f(); // Constructing a // Constructing b // Constructing c // Constructing d // Disposing (async) d // Disposing (async) c // Constructing e // Disposing (async) e // Disposing (async) b // Disposing (async) a |
Définir les types en termes de Disposable et AsyncDisposable peut rendre votre code beaucoup plus facile à travailler si vous attendez des autres qu'ils fassent de la logique de destruction de manière cohérente. En fait, il existe de nombreux types existants dans la nature qui ont une méthode dispose( ) ou close( ). Par exemple, les API de Visual Studio Code définissent même leur propre interface Disposable. Les API dans le navigateur et dans les moteurs d'exécution comme Node.js, Deno et Bun peuvent également choisir d'utiliser Symbol.dispose et Symbol.asyncDispose pour les objets qui ont déjà des méthodes de nettoyage, comme les poignées de fichiers, les connexions, et plus encore.
Peut-être que tout cela semble excellent pour les bibliothèques, mais un peu lourd pour vos scénarios. Si vous faites beaucoup de nettoyage ad-hoc, la création d'un nouveau type pourrait introduire beaucoup de sur-abstraction et des questions sur les meilleures pratiques. Reprenons l'exemple de TempFile.
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 | class TempFile implements Disposable { #path: string; #handle: number; constructor(path: string) { this.#path = path; this.#handle = fs.openSync(path, "w+"); } // other methods [Symbol.dispose]() { // Close the file and delete it. fs.closeSync(this.#handle); fs.unlinkSync(this.#path); } } export function doSomeWork() { using file = new TempFile(".some_temp_file"); // use file... if (someCondition()) { // do some more work... return; } } |
Tout ce que nous voulions, c'était nous souvenir d'appeler deux fonctions - mais était-ce la meilleure façon de l'écrire ? Devrions-nous appeler openSync dans le constructeur, créer une méthode open( ), ou passer la poignée nous-mêmes ? Devrions-nous exposer une méthode pour chaque opération possible que nous devons effectuer, ou devrions-nous simplement rendre les propriétés publiques ?
Cela nous amène aux dernières étoiles de la fonctionnalité : DisposableStack et AsyncDisposableStack. Ces objets sont utiles pour effectuer à la fois un nettoyage ponctuel et des quantités arbitraires de nettoyage. Un DisposableStack est un objet qui possède plusieurs méthodes pour garder une trace des objets Disposable, et qui peut recevoir des fonctions pour effectuer un travail de nettoyage arbitraire. Nous pouvons également les assigner à des variables using parce que - tenez-vous bien - elles sont également Disposable ! Voici donc comment nous aurions pu écrire l'exemple original.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function doSomeWork() { const path = ".some_temp_file"; const file = fs.openSync(path, "w+"); using cleanup = new DisposableStack(); cleanup.defer(() => { fs.closeSync(file); fs.unlinkSync(path); }); // use file... if (someCondition()) { // do some more work... return; } // ... } |
La méthode defer est similaire à bien des égards au mot-clé defer dans Go, Swift, Zig, Odin, et d'autres, où les conventions devraient être similaires.
Cette fonctionnalité étant très récente, la plupart des systèmes d'exécution ne la supporteront pas nativement. Pour l'utiliser, vous aurez besoin de polyfills pour les éléments suivants :
- Symbol.dispose
- Symbol.asyncDispose
- DisposableStack
- AsyncDisposableStack
- SuppressedError
Cependant, si tout ce qui vous intéresse est using et await using, vous devriez pouvoir vous en sortir en ne remplissant que les symbols intégrés. Quelque chose d'aussi simple que ce qui suit devrait fonctionner dans la plupart des cas :
Code : | Sélectionner tout |
1 2 | Symbol.dispose ??= Symbol("Symbol.dispose"); Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose"); |
Code : | Sélectionner tout |
1 2 3 4 5 6 | { "compilerOptions": { "target": "es2022", "lib": ["es2022", "esnext.disposable", "dom"] } } |
TypeScript 5.2 met en œuvre une fonctionnalité ECMAScript à venir appelée métadonnées de décorateur.
L'idée principale de cette fonctionnalité est de permettre aux décorateurs de créer et de consommer facilement des métadonnées sur n'importe quelle classe sur laquelle ils sont utilisés ou à l'intérieur de celle-ci.
Chaque fois que des fonctions de décorateur sont utilisées, elles ont désormais accès à une nouvelle propriété de metadata sur leur objet de contexte. La propriété metadata contient un simple objet. Comme JavaScript nous permet d'ajouter des propriétés de manière arbitraire, il peut être utilisé comme un dictionnaire mis à jour par chaque décorateur. Alternativement, puisque chaque objet de metadata sera identique pour chaque portion décorée d'une classe, il peut être utilisé comme une clé dans une Map. Une fois que tous les décorateurs sur ou dans une classe ont été exécutés, cet objet peut être accédé sur la classe via Symbol.metadata.
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 | interface Context { name: string; metadata: Record<PropertyKey, unknown>; } function setMetadata(_target: any, context: Context) { context.metadata[context.name] = true; } class SomeClass { @setMetadata foo = 123; @setMetadata accessor bar = "hello!"; @setMetadata baz() { } } const ourMetadata = SomeClass[Symbol.metadata]; console.log(JSON.stringify(ourMetadata)); // { "bar": true, "baz": true, "foo": true } |
Par exemple, disons que nous voulions utiliser les décorateurs pour garder une trace des propriétés et des accesseurs sérialisables lorsque nous utilisons JSON.stringify de la manière suivante :
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 | mport { serialize, jsonify } from "./serializer"; class Person { firstName: string; lastName: string; @serialize age: number @serialize get fullName() { return `${this.firstName} ${this.lastName}`; } toJSON() { return jsonify(this) } constructor(firstName: string, lastName: string, age: number) { // ... } } |
Voici un exemple de définition du module ./serialize.ts :
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 | const serializables = Symbol(); type Context = | ClassAccessorDecoratorContext | ClassGetterDecoratorContext | ClassFieldDecoratorContext ; export function serialize(_target: any, context: Context): void { if (context.static || context.private) { throw new Error("Can only serialize public instance members.") } if (typeof context.name === "symbol") { throw new Error("Cannot serialize symbol-named properties."); } const propNames = (context.metadata[serializables] as string[] | undefined) ??= []; propNames.push(context.name); } export function jsonify(instance: object): string { const metadata = instance.constructor[Symbol.metadata]; const propNames = metadata?.[serializables] as string[] | undefined; if (!propNames) { throw new Error("No members marked with @serialize."); } const pairStrings = propNames.map(key => { const strKey = JSON.stringify(key); const strValue = JSON.stringify((instance as any)[key]); return `${strKey}: ${strValue}`; }); return `{ ${pairStrings.join(", ")} }`; } |
L'utilisation d'un symbole rend techniquement ces données accessibles à d'autres personnes. Une alternative pourrait être d'utiliser un WeakMap en utilisant l'objet de métadonnées comme clé. Cela permet de garder les données privées et d'utiliser moins d'assertions de type dans ce cas, mais c'est autrement similaire.
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 37 | const serializables = new WeakMap<object, string[]>(); type Context = | ClassAccessorDecoratorContext | ClassGetterDecoratorContext | ClassFieldDecoratorContext ; export function serialize(_target: any, context: Context): void { if (context.static || context.private) { throw new Error("Can only serialize public instance members.") } if (typeof context.name !== "string") { throw new Error("Can only serialize string properties."); } const propNames = serializables.get(context.metadata); if (propNames === undefined) { serializables.set(context.metadata, propNames = []); } propNames.push(context.name); } export function jsonify(instance: object): string { const metadata = instance.constructor[Symbol.metadata]; const propNames = metadata && serializables.get(metadata); if (!propNames) { throw new Error("No members marked with @serialize."); } const pairStrings = propNames.map(key => { const strKey = JSON.stringify(key); const strValue = JSON.stringify((instance as any)[key]); return `${strKey}: ${strValue}`; }); return `{ ${pairStrings.join(", ")} }`; } |
Cette fonctionnalité étant encore récente, la plupart des systèmes d'exécution ne la supporteront pas nativement. Pour l'utiliser, vous aurez besoin d'un polyfill pour Symbol.metadata. Quelque chose d'aussi simple que ce qui suit devrait fonctionner dans la plupart des cas :
Code : | Sélectionner tout |
Symbol.metadata ??= Symbol("Symbol.metadata");
Code : | Sélectionner tout |
1 2 3 4 5 6 | { "compilerOptions": { "target": "es2022", "lib": ["es2022", "esnext.decorators", "dom"] } } |
Les types de tuple prennent en charge des étiquettes ou des noms facultatifs pour chaque élément.
Code : | Sélectionner tout |
type Pair<T> = [first: T, second: T];
Cependant, TypeScript avait auparavant une règle selon laquelle les tuples ne pouvaient pas mélanger des éléments étiquetés et non étiquetés. En d'autres termes, soit aucun élément ne pouvait avoir d'étiquette dans un tuple, soit tous les éléments devaient en avoir une.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 | // fine - no labels type Pair1<T> = [T, T]; // fine - all fully labeled type Pair2<T> = [first: T, second: T]; // previously an error type Pair3<T> = [first: T, T]; // ~ // Tuple members must all have names // or all not have names. |
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | // previously an error type TwoOrMore_A<T> = [first: T, second: T, ...T[]]; // ~~~~~~ // Tuple members must all have names // or all not have names. // type TwoOrMore_B<T> = [first: T, second: T, rest: ...T[]]; |
Code : | Sélectionner tout |
1 2 3 4 5 6 | type HasLabels = [a: string, b: string]; type HasNoLabels = [number, number]; type Merged = [...HasNoLabels, ...HasLabels]; // ^ [number, number, string, string] // // 'a' and 'b' were lost in 'Merged' |
Utilisation plus facile des méthodes pour les unions de tableaux
Dans les versions précédentes de TypeScript, l'appel d'une méthode sur une union de tableaux pouvait s'avérer pénible.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | declare let array: string[] | number[]; array.filter(x => !!x); // ~~~~~~ error! // This expression is not callable. // Each member of the union type '...' has signatures, // but none of those signatures are compatible // with each other. |
Dans cet exemple, TypeScript essaierait de voir si chaque version de filter est compatible avec string[ ] et number[ ]. Sans stratégie cohérente, TypeScript a jeté ses mains en l'air et a dit "je ne peux pas le faire fonctionner".
Dans TypeScript 5.2, avant d'abandonner dans ces cas, les unions de tableaux sont traitées comme un cas spécial. Un nouveau type de tableau est construit à partir du type d'élément de chaque membre, puis la méthode est invoquée sur celui-ci.
Dans l'exemple ci-dessus, string[ ] | number[ ] est transformé en (string | number)[ ] (ou Array<string | number>, et filter est invoqué sur ce type. Il y a un léger inconvénient : filter produira un Array<string | number> au lieu d'un string[] | number[] ; mais pour une valeur fraîchement produite, il y a moins de risque que quelque chose "tourne mal".
Cela signifie que de nombreuses méthodes telles que filter, find, some, every et reduce devraient toutes pouvoir être invoquées sur des unions de tableaux dans des cas où elles ne l'étaient pas auparavant.
Compléments à la virgule pour les membres d'un objet
Il peut être facile d'oublier d'ajouter une virgule lors de l'ajout d'une nouvelle propriété à un objet. Auparavant, si vous oubliiez une virgule et que vous demandiez l'auto-complétion, TypeScript donnait des résultats de complétion médiocres et sans rapport avec le sujet.
TypeScript 5.2 fournit désormais de manière élégante des compléments sur les membres d'un objet lorsqu'il manque une virgule. Mais pour éviter de vous infliger une erreur de syntaxe, TypeScript insère automatiquement la virgule manquante.
Refactorisation des variables en ligne
TypeScript 5.2 dispose désormais d'un refactoring permettant d'intégrer le contenu d'une variable dans tous les sites d'utilisation.
L'utilisation du refactoring "inline variable" éliminera la variable et remplacera toutes les utilisations de la variable par son initialisateur. Notez que cela peut entraîner l'exécution des effets secondaires de l'initialisateur à un moment différent et autant de fois que la variable a été utilisée.
Changements de rupture et corrections
TypeScript s'efforce de ne pas introduire inutilement des ruptures ; cependant, nous devons parfois apporter des corrections et des améliorations afin que le code puisse être mieux analysé
Changements dans lib.d.ts
Les types générés pour le DOM peuvent avoir un impact sur votre base de code.
labeledElementDeclarations peut contenir des éléments undefined
Afin de supporter un mélange d'éléments étiquetés et non étiquetés, l'API de TypeScript a légèrement changé. La propriété labeledElementDeclarations de TupleType peut contenir des éléments non définis à chaque position où un élément n'est pas étiqueté.
Code : | Sélectionner tout |
1 2 3 | interface TupleType { - labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration)[]; + labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration | undefined)[]; |
Les options --module et --moduleResolution supportent chacune les paramètres node16 et nodenext. Il s'agit en fait de paramètres "Node.js modernes" qui devraient être utilisés dans tout projet Node.js récent. Nous avons constaté que lorsque ces deux options ne s'accordent pas sur l'utilisation des paramètres liés à Node.js, les projets sont effectivement mal configurés.
Dans TypeScript 5.2, lorsque l'on utilise node16 ou nodenex pour les options --module et --moduleResolution, TypeScript exige désormais que l'autre option ait un paramètre similaire lié à Node.js. Dans les cas où les paramètres divergent, vous obtiendrez probablement un message d'erreur du type.
Code : | Sélectionner tout |
Option 'moduleResolution' must be set to 'NodeNext' (or left unspecified) when option 'module' is set to 'NodeNext'.
Code : | Sélectionner tout |
Option 'module' must be set to 'Node16' when option 'moduleResolution' is set to 'Node16'.
Vérification cohérente de l'exportation des symboles fusionnés
Lorsque deux déclarations fusionnent, elles doivent s'accorder sur le fait qu'elles sont toutes deux exportées. En raison d'un bogue, TypeScript a manqué des cas spécifiques dans des contextes ambiants, comme dans les fichiers de déclaration ou les blocs declare module. Par exemple, il n'émettrait pas d'erreur dans un cas comme celui-ci, où replaceInFile est déclaré une fois en tant que fonction exportée, et une fois en tant qu'espace de noms non exporté.
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 | declare module 'replace-in-file' { export function replaceInFile(config: unknown): Promise<unknown[]>; export {}; namespace replaceInFile { export function sync(config: unknown): unknown[]; } } |
Dans un module ambiant, l'ajout d'un export { ... } ou une construction similaire comme export default ... modifie implicitement si toutes les déclarations sont automatiquement exportées. TypeScript reconnaît maintenant cette sémantique malheureusement déroutante de manière plus cohérente, et émet une erreur sur le fait que toutes les déclarations de replaceInFile doivent être en accord dans leurs modificateurs, et émettra l'erreur suivante :
Code : | Sélectionner tout |
Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.
À ce stade, TypeScript 5.2 est ce que nous appellerions "stable en termes de fonctionnalités". TypeScript 5.2 se concentrera sur les corrections de bogues, le polissage et certaines fonctionnalités d'édition à faible risque. Une release 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 le 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.2 jusqu'à la publication de la release candidate. Les nightlies sont bien testées et peuvent même être testées uniquement dans votre éditeur.
Joyeux hacking !
- Daniel Rosenwasser et l'équipe TypeScript
Source : Microsoft
Et vous ?
Quel est votre avis sur le sujet ?
Que pensez-vous des fonctionnalités proposées par cette version de TypeScript ?
Voir aussi :
Microsoft annonce la disponibilité de TypeScript 5.0. Et présente les principaux changements notables depuis la publication de la version bêta
Microsoft annonce la disponibilité de la version 5.1 de TypeScript, et présente les changements depuis les versions bêta et release candidate