IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

Vous êtes nouveau sur Developpez.com ? Créez votre compte ou connectez-vous afin de pouvoir participer !

Vous devez avoir un compte Developpez.com et être connecté pour pouvoir participer aux discussions.

Vous n'avez pas encore de compte Developpez.com ? Créez-en un en quelques instants, c'est entièrement gratuit !

Si vous disposez déjà d'un compte et qu'il est bien activé, connectez-vous à l'aide du formulaire ci-dessous.

Identifiez-vous
Identifiant
Mot de passe
Mot de passe oublié ?
Créer un compte

L'inscription est gratuite et ne vous prendra que quelques instants !

Je m'inscris !

Microsoft annonce la disponibilité de TypeScript 5.0
Et présente les principaux changements notables depuis la publication de la version bêta

Le , par Anthony

66PARTAGES

13  0 
Microsoft annonce la sortie de la version bêta de TypeScript 5.0, et apporte un nouveau standard pour les décorateurs en plus de nombreuses autres améliorations

Aujourd'hui, nous sommes heureux d'annoncer la version bêta de TypeScript 5.0 ! Cette version apporte de nombreuses nouvelles fonctionnalités, tout en visant à rendre TypeScript, plus léger, plus simple et plus rapide. Nous avons implémenté le nouveau standard des décorateurs, une fonctionnalité pour mieux supporter les projets ESM dans Node et les bundlers, de nouvelles façons pour les auteurs de bibliothèques de contrôler l'inférence générique, nous avons étendu notre fonctionnalité JSDoc, simplifié la configuration et apporté de nombreuses autres améliorations.

Bien que la version 5.0 comprenne des modifications de correction et des dépréciations pour les flags moins utilisés, nous pensons que la plupart des utilisateurs auront une expérience de mise à niveau similaire à celle des versions précédentes.

Pour commencer à utiliser la version bêta, vous pouvez l'obtenir via NuGet, ou utiliser npm avec la commande suivante :

Code : Sélectionner tout
npm install typescript@beta

Voici une liste rapide de toutes les nouveautés de TypeScript 5.0 !

  • Décorateurs
  • Paramètres de type const
  • Prise en charge de plusieurs fichiers de configuration dans extends
  • Tous les enums sont des enums d'union
  • bundler --moduleResolution
  • Flags de personnalisation de la résolution
  • --verbatimModuleSyntax
  • Prise en charge de export type *
  • Support de @satisfies dans JSDoc
  • Support de @overload dans JSDoc
  • Passage des flags spécifiques à l'émission sous --build
  • Complétions exhaustives de switch/case
  • Optimisations de la vitesse, de la mémoire et de la taille des paquets

Décorateurs

Les décorateurs sont une fonctionnalité ECMAScript à venir qui nous permet de personnaliser les classes et leurs membres de manière réutilisable.

Considérons le code suivant :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
class Person { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
    } 
 
    greet() { 
        console.log(`Hello, my name is ${this.name}.`); 
    } 
} 
 
const p = new Person("Ray"); 
p.greet();

La méthode greet est assez simple ici, mais imaginons qu'il s'agisse de quelque chose de plus compliqué - peut-être qu'elle fait de la logique asynchrone, qu'elle est récursive, qu'elle a des effets de bord, etc. Indépendamment du type de boue que vous imaginez, disons que vous ajoutez quelques appels à console.log pour aider à déboguer la méthode greet.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Person { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
    } 
 
    greet() { 
        console.log("LOG: Entering method."); 
 
        console.log(`Hello, my name is ${this.name}.`); 
 
        console.log("LOG: Exiting method.") 
    } 
}

Ce modèle est assez commun. Ce serait bien s'il y avait un moyen de le faire pour chaque méthode !

C'est là que les décorateurs interviennent. Nous pouvons écrire une fonction appelée loggedMethod qui ressemble à ce qui suit :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
function loggedMethod(originalMethod: any, _context: any) { 
 
    function replacementMethod(this: any, ...args: any[]) { 
        console.log("LOG: Entering method.") 
        const result = originalMethod.call(this, ...args); 
        console.log("LOG: Exiting method.") 
        return result; 
    } 
 
    return replacementMethod; 
}

"C'est quoi le problème avec tous ces anys ? Qu'est-ce que c'est, anyScript ! ?"

Soyez patient - nous gardons les choses simples pour l'instant afin de pouvoir nous concentrer sur ce que fait cette fonction. Remarquez que loggedMethod prend la méthode originale (originalMethod) et renvoie une fonction qui

  1. enregistre un message "Entering..." (entrée)
  2. transmet this ainsi que tous ses arguments à la méthode originale
  3. enregistre un message "Exiting...", et
  4. renvoie ce que la méthode originale a retourné.

Nous pouvons maintenant utiliser loggedMethod pour décorer la méthode greet :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
    } 
 
    @loggedMethod 
    greet() { 
        console.log(`Hello, my name is ${this.name}.`); 
    } 
} 
 
const p = new Person("Ray"); 
p.greet(); 
 
// Output: 
// 
//   LOG: Entering method. 
//   Hello, my name is Ray. 
//   LOG: Exiting method.

Nous venons d'utiliser loggedMethod comme décorateur au-dessus de greet - et remarquez que nous l'avons écrit sous la forme @loggedMethod. Lorsque nous avons fait cela, il a été appelé avec la cible de la méthode et un objet de contexte. Parce que loggedMethod a retourné une nouvelle fonction, cette fonction a remplacé la définition originale de greet.

Nous ne l'avons pas encore mentionné, mais loggedMethod a été défini avec un deuxième paramètre. Il s'agit d'un "objet de contexte", qui contient des informations utiles sur la façon dont la méthode décorée a été déclarée - par exemple, s'il s'agit d'un membre #privé, ou statique, ou encore le nom de la méthode. Réécrivons loggedMethod pour en tirer parti et afficher le nom de la méthode qui a été décorée.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
function loggedMethod(originalMethod: any, context: ClassMethodDecoratorContext) { 
    const methodName = String(context.name); 
 
    function replacementMethod(this: any, ...args: any[]) { 
        console.log(`LOG: Entering method '${methodName}'.`) 
        const result = originalMethod.call(this, ...args); 
        console.log(`LOG: Exiting method '${methodName}'.`) 
        return result; 
    } 
 
    return replacementMethod; 
}

Nous utilisons maintenant le paramètre de contexte - et c'est la première chose dans loggedMethod qui a un type plus strict que any et any[]. TypeScript fournit un type appelé ClassMethodDecoratorContext qui modélise l'objet de contexte que les décorateurs de méthodes prennent.

Outre les métadonnées, l'objet de contexte pour les méthodes possède également une fonction très utile appelée addInitializer. C'est un moyen de s'accrocher au début du constructeur (ou de l'initialisation de la classe elle-même si nous travaillons avec des statics).

Par exemple, en JavaScript, il est courant d'écrire quelque chose comme le modèle suivant :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
class Person { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
 
        this.greet = this.greet.bind(this); 
    } 
 
    greet() { 
        console.log(`Hello, my name is ${this.name}.`); 
    } 
}

Alternativement, greet pourrait être déclaré comme une propriété initialisée avec une fonction arrow.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
class Person { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
    } 
 
    greet = () => { 
        console.log(`Hello, my name is ${this.name}.`); 
    }; 
}

Ce code est écrit pour s'assurer que this n'est pas lié à nouveau si greet est appelé en tant que fonction autonome ou passé en tant que callback.

Code : Sélectionner tout
1
2
3
4
const greet = new Person("Ray").greet; 
 
// We don't want this to fail! 
greet();

Nous pouvons écrire un décorateur qui utilise addInitializer pour appeler bind dans le constructeur à notre place.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
function bound(originalMethod: any, context: ClassMethodDecoratorContext) { 
    const methodName = context.name; 
    if (context.private) { 
        throw new Error(`'bound' cannot decorate private properties like ${methodName as string}.`); 
    } 
    context.addInitializer(function () { 
        this[methodName] = this[methodName].bind(this); 
    }); 
}

bound ne renvoie rien. Par conséquent, lorsqu'il décore une méthode, il ne touche pas à l'original. Au lieu de cela, il ajoutera une logique avant que tout autre champ ne soit initialisé.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Person { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
    } 
 
    @bound 
    @loggedMethod 
    greet() { 
        console.log(`Hello, my name is ${this.name}.`); 
    } 
} 
 
const p = new Person("Ray"); 
const greet = p.greet; 
 
// Works! 
greet();

Remarquez que nous avons empilé deux décorateurs - @bound et @loggedMethod. Ces décorations s'exécutent dans un "ordre inverse". Autrement dit, @loggedMethod décore la méthode originale greet, et @bound décore le résultat de @loggedMethod. Dans cet exemple, cela n'a pas d'importance, mais cela pourrait en avoir si vos décorateurs ont des effets de bord ou s'ils attendent un certain ordre.

Il convient également de noter que, si vous le préférez d'un point de vue stylistique, vous pouvez placer ces décorateurs sur la même ligne.

Code : Sélectionner tout
1
2
3
@bound @loggedMethod greet() { 
        console.log(`Hello, my name is ${this.name}.`); 
    }

Ce qui n'est peut-être pas évident, c'est que nous pouvons même créer des fonctions qui retournent des fonctions décoratrices. Cela permet de personnaliser légèrement le décorateur final. Si nous le voulions, nous aurions pu faire en sorte que loggedMethod renvoie un décorateur et personnaliser la façon dont il enregistre ses messages.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
function loggedMethod(headMessage = "LOG:") { 
    return function actualDecorator(originalMethod: any, context: ClassMethodDecoratorContext) { 
        const methodName = String(context.name); 
 
        function replacementMethod(this: any, ...args: any[]) { 
            console.log(`${headMessage} Entering method '${methodName}'.`) 
            const result = originalMethod.call(this, ...args); 
            console.log(`${headMessage} Exiting method '${methodName}'.`) 
            return result; 
        } 
 
        return replacementMethod; 
    } 
}

Si nous faisions cela, nous devrions appeler loggedMethod avant de l'utiliser comme décorateur. Nous pourrions alors passer n'importe quel string comme préfixe pour les messages qui sont enregistrés dans la console.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Person { 
    name: string; 
    constructor(name: string) { 
        this.name = name; 
    } 
 
    @loggedMethod("") 
    greet() { 
        console.log(`Hello, my name is ${this.name}.`); 
    } 
} 
 
const p = new Person("Ray"); 
p.greet(); 
 
// Output: 
// 
//    Entering method 'greet'. 
//   Hello, my name is Ray. 
//    Exiting method 'greet'.

Les décorateurs ne sont pas seulement utilisables avec les méthodes ! Ils peuvent être utilisés sur les propriétés/champs, les getters, les setters et les auto-accesseurs. Les classes elles-mêmes peuvent être décorées pour des choses comme le sous-classement et l'enregistrement.

Différences avec les anciens décorateurs expérimentaux

Si vous utilisez TypeScript depuis un certain temps, vous savez peut-être qu'il prend en charge les décorateurs "expérimentaux" depuis des années. Bien que ces décorateurs expérimentaux aient été incroyablement utiles, ils ont modélisé une version beaucoup plus ancienne de la proposition de décorateurs, et ont toujours nécessité un flag de compilation opt-in appelé --experimentalDecorators. Toute tentative d'utilisation des décorateurs dans TypeScript sans ce flag entraînait un message d'erreur.

--experimentalDecorators continuera à exister dans un avenir proche ; cependant, sans ce flag, les décorateurs seront désormais considérés comme une syntaxe valide pour tout nouveau code. En dehors de --experimentalDecorators, ils seront vérifiés au niveau du type et émis différemment. Les règles de vérification de type et d'émission sont suffisamment différentes pour que, même si les décorateurs peuvent être écrits pour supporter à la fois l'ancien et le nouveau comportement des décorateurs, il est peu probable que les fonctions de décorateurs existantes le fassent.

Cette nouvelle proposition de décorateurs n'est pas compatible avec --emitDecoratorMetadata, et elle n'autorise pas les paramètres de décoration. Les futures propositions de l'ECMAScript pourront peut-être aider à combler cette lacune.

Une dernière remarque : pour l'instant, la proposition relative aux décorateurs exige qu'un décorateur de classe vienne après le mot-clé export s'il est présent.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
export @register class Foo { 
    // ... 
} 
 
export 
@Component({ 
    // ... 
}) 
class Bar { 
    // ... 
}

TypeScript appliquera cette restriction dans les fichiers JavaScript, mais ne le fera pas pour les fichiers TypeScript. Une partie de ceci est motivée par les utilisateurs existants - nous espérons fournir un chemin de migration légèrement plus facile entre nos décorateurs "expérimentaux" originaux et les décorateurs standardisés. En outre, de nombreux utilisateurs nous ont fait part de leur préférence pour le style original, et nous espérons pouvoir discuter de cette question en toute bonne foi lors des futures discussions sur les normes.

Écrire des décorateurs bien typés

Les exemples de décorateurs loggedMethod et bound ci-dessus sont intentionnellement simples et omettent beaucoup de détails sur les types.

Le typage des décorateurs peut être assez complexe. Par exemple, une version bien typée du décorateur loggedMethod ci-dessus pourrait ressembler à quelque chose comme ceci :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function loggedMethod<This, Args extends any[], Return>( 
    target: (this: This, ...args: Args) => Return, 
    context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return> 
) { 
    const methodName = String(context.name); 
 
    function replacementMethod(this: This, ...args: Args): Return { 
        console.log(`LOG: Entering method '${methodName}'.`) 
        const result = target.call(this, ...args); 
        console.log(`LOG: Exiting method '${methodName}'.`) 
        return result; 
    } 
 
    return replacementMethod; 
}

Nous avons dû modéliser séparément le type de this, les paramètres et le type de retour de la méthode originale, en utilisant les paramètres de type This, Args et Return.

La complexité exacte de la définition de vos fonctions décoratrices dépend de ce que vous voulez garantir. Gardez à l'esprit que vos décorateurs seront plus utilisés qu'ils ne sont écrits, donc une version bien typée sera généralement préférable - mais il y a clairement un compromis avec la lisibilité, donc essayez de garder les choses simples.

Paramètres de type const

Lorsqu'il déduit le type d'un objet, TypeScript choisit généralement un type qui est censé être général. Par exemple, dans ce cas, le type inféré de names est string[] :

Code : Sélectionner tout
1
2
3
4
5
6
7
type HasNames = { readonly names: string[] }; 
function getNamesExactly<T extends HasNames>(arg: T): T["names"] { 
    return arg.names; 
} 
 
// Inferred type: string[] 
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"]});

Habituellement, l'intention est de permettre la mutation en aval.

Cependant, en fonction de ce que fait exactement getNamesExactly et de la manière dont elle est censée être utilisée, il peut souvent arriver qu'un type plus spécifique soit souhaité.

Jusqu'à présent, les auteurs d'API devaient généralement recommander d'ajouter as const à certains endroits pour obtenir l'inférence souhaitée :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
// The type we wanted: 
//    readonly ["Alice", "Bob", "Eve"] 
// The type we got: 
//    string[] 
const names1 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]}); 
 
// Correctly gets what we wanted: 
//    readonly ["Alice", "Bob", "Eve"] 
const names2 = getNamesExactly({ names: ["Alice", "Bob", "Eve"]} as const);

Cela peut être fastidieux et facile à oublier. Dans TypeScript 5.0, vous pouvez désormais ajouter un modificateur const à une déclaration de paramètre de type pour que l'inférence de type const soit la valeur par défaut :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
type HasNames = { names: readonly string[] }; 
function getNamesExactly<const T extends HasNames>(arg: T): T["names"] { 
//                       ^^^^^ 
    return arg.names; 
} 
 
// Inferred type: readonly ["Alice", "Bob", "Eve"] 
// Note: Didn't need to write 'as const' here 
const names = getNamesExactly({ names: ["Alice", "Bob", "Eve"] });

Notez que le modificateur const ne rejette pas les valeurs mutables et n'exige pas de contraintes immuables. L'utilisation d'une contrainte de type mutable peut donner des résultats surprenants. Par exemple :

Code : Sélectionner tout
1
2
3
4
declare function fnBad<const T extends string[]>(args: T): void; 
 
// 'T' is still 'string[]' since 'readonly ["a", "b", "c"]' is not assignable to 'string[]' 
fnBad(["a", "b" ,"c"]);

Ici, le candidat inféré pour T est readonly ["a", "b", "c"], et un tableau readonly ne peut pas être utilisé là où un tableau mutable est nécessaire. Dans ce cas, l'inférence est ramenée à la contrainte, la chaîne est traitée comme un string[], et l'appel se déroule toujours avec succès.

Une meilleure définition de cette fonction devrait utiliser readonly string[] :

Code : Sélectionner tout
1
2
3
4
declare function fnGood<const T extends readonly string[]>(args: T): void; 
 
// T is readonly ["a", "b", "c"] 
fnGood(["a", "b" ,"c"]);

De même, n'oubliez pas que le modificateur const n'affecte que l'inférence des expressions d'objets, de tableaux et de primitives qui ont été écrites dans l'appel, donc les arguments qui ne seraient pas (ou ne pourraient pas) être modifiés avec as const ne verront pas de changement de comportement :

Code : Sélectionner tout
1
2
3
4
5
declare function fnGood<const T extends readonly string[]>(args: T): void; 
const arr = ["a", "b" ,"c"]; 
 
// 'T' is still 'string[]'-- the 'const' modifier has no effect here 
fnGood(arr);

Prise en charge de plusieurs fichiers de configuration dans extends

Lorsque vous gérez plusieurs projets, il peut être utile de disposer d'un fichier de configuration "base" à partir duquel les autres fichiers tsconfig.json peuvent s'étendre. C'est pourquoi TypeScript prend en charge un champ extends pour copier les champs de compilerOptions.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
// packages/front-end/src/tsconfig.json 
{ 
    "compilerOptions": { 
        "extends": "../../../tsconfig.base.json", 
 
        "outDir": "../lib", 
        // ... 
    } 
}

Cependant, il existe des scénarios dans lesquels vous pourriez vouloir étendre à partir de plusieurs fichiers de configuration. Par exemple, imaginez que vous utilisez un fichier de configuration de base TypeScript fourni par npm. Si vous souhaitez que tous vos projets utilisent également les options du paquet @tsconfig/strictest sur npm, il existe une solution simple : faites en sorte que tsconfig.base.json étende @tsconfig/strictest :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
// tsconfig.base.json 
{ 
    "compilerOptions": { 
        "extends": "@tsconfig/strictest/tsconfig.json", 
 
        // ... 
    } 
}

Cela fonctionne jusqu'à un certain point. Si vous avez des projets qui ne veulent pas utiliser @tsconfig/strictest, ils doivent soit désactiver manuellement les options, soit créer une version séparée de tsconfig.base.json qui ne s'étend pas à partir de @tsconfig/strictest.

Pour plus de souplesse, Typescript 5.0 permet désormais au champ extends de prendre plusieurs entrées. Par exemple, dans ce fichier de configuration :

Code : Sélectionner tout
1
2
3
4
5
{ 
    "compilerOptions": { 
        "extends": ["a", "b", "c"] 
    } 
}

Écrire ceci revient à étendre directement c, où c étend b, et b étend a. Si l'un des champs est " en conflit ", la dernière entrée l'emporte.

Ainsi, dans l'exemple suivant, strictNullChecks et noImplicitAny sont tous deux activés dans le fichier tsconfig.json final.

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
// tsconfig1.json 
{ 
    "compilerOptions": { 
        "strictNullChecks": true 
    } 
} 
 
// tsconfig2.json 
{ 
    "compilerOptions": { 
        "noImplicitAny": true 
    } 
} 
 
// tsconfig.json 
{ 
    "compilerOptions": { 
        "extends": ["./tsconfig1.json", "./tsconfig2.json"] 
    }, 
    "files": ["./index.ts"] 
}

À titre d'exemple, nous pouvons réécrire notre exemple original de la manière suivante.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
// packages/front-end/src/tsconfig.json 
{ 
    "compilerOptions": { 
        "extends": ["@tsconfig/strictest/tsconfig.json", "../../../tsconfig.base.json"], 
 
        "outDir": "../lib", 
        // ... 
    } 
}

Tous les enums sont des enums d'union

Lorsque TypeScript a initialement introduit les enums, ils n'étaient rien de plus qu'un ensemble de constantes numériques avec le même type.

Code : Sélectionner tout
1
2
3
4
enum E { 
    Foo = 10, 
    Bar = 20, 
}

La seule chose spéciale à propos de E.Foo et E.Bar était qu'ils étaient affectables à tout ce qui attendait le type E. En dehors de cela, ils étaient à peu près juste des numbers.

Code : Sélectionner tout
1
2
3
4
function takeValue(e: E) {} 
 
takeValue(E.Foo); // works 
takeValue(123);   // error!

Ce n'est que lorsque TypeScript 2.0 a introduit les types littéraux d'enum que les enums sont devenus un peu plus spéciaux. Les types littéraux d'enum donnent à chaque membre de l'enum son propre type, et transforment l'enum elle-même en une union du type de chaque membre. Ils nous permettent également de nous référer uniquement à un sous-ensemble de types d'une enum, et de réduire ces types.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Color is like a union of Red | Orange | Yellow | Green | Blue | Violet 
enum Color { 
    Red, Orange, Yellow, Green, Blue, /* Indigo */, Violet 
} 
 
// Each enum member has its own type that we can refer to! 
type PrimaryColor = Color.Red | Color.Green | Color.Blue; 
 
function isPrimaryColor(c: Color): C is PrimaryColor { 
    // Narrowing literal types can catch bugs. 
    // TypeScript will error here because 
    // we'll end up comparing 'Color.Red' to 'Color.Green'. 
    // We meant to use ||, but accidentally wrote &&. 
    return c === Color.Red && c === Color.Green && c === Color.Blue; 
}

Le fait de donner à chaque membre d'une enum son propre type posait un problème : ces types étaient en partie associés à la valeur réelle du membre. Dans certains cas, il n'est pas possible de calculer cette valeur - par exemple, un membre d'une énumération peut être initialisé par un appel de fonction.

Code : Sélectionner tout
1
2
3
enum E { 
    Blah = Math.random() 
}

Chaque fois que TypeScript rencontrait ces problèmes, il faisait discrètement demi-tour et utilisait l'ancienne stratégie d'enum. Cela signifiait renoncer à tous les avantages des unions et des types littéraux.

TypeScript 5.0 parvient à transformer tous les enums en unions d'enums en créant un type unique pour chaque membre calculé. Cela signifie que tous les enums peuvent désormais être réduits et que leurs membres sont également référencés en tant que types.

Bundler --moduleResolution

TypeScript 4.7 a introduit les options node16 et nodenext pour ses options --module et --moduleResolution. L'intention de ces options était de mieux modéliser les règles de recherche précises pour les modules ECMAScript dans Node.js ; cependant, ce mode a de nombreuses restrictions que les autres outils n'appliquent pas vraiment.

Par exemple, dans un module ECMAScript de Node.js, toute importation relative doit inclure une extension de fichier.

Code : Sélectionner tout
1
2
3
4
// entry.mjs 
import * as utils from "./utils";     //  wrong - we need to include the file extension. 
 
import * as utils from "./utils.mjs"; //  works

Il y a certaines raisons à cela dans Node.js et dans le navigateur - cela accélère la recherche de fichiers et fonctionne mieux pour les serveurs de fichiers naïfs. Mais pour de nombreux développeurs utilisant des outils comme les bundlers, les options node16/nodenext étaient encombrantes car les bundlers n'ont pas la plupart de ces restrictions. D'une certaine manière, le mode de résolution de node était meilleur pour quiconque utilise un bundler.

Mais d'une certaine manière, le mode de résolution original de node était déjà dépassé. La plupart des bundlers modernes utilisent une fusion du module ECMAScript et des règles de recherche CommonJS dans Node.js. Par exemple, les importations sans extension fonctionnent très bien comme dans CommonJS, mais lorsqu'on consulte les conditions d'exportation d'un paquet, on préfère une condition d'import comme dans un fichier ECMAScript.

Pour modéliser le fonctionnement des bundlers, TypeScript introduit désormais une nouvelle stratégie : le bundler --moduleResolution.

Code : Sélectionner tout
1
2
3
4
5
6
{ 
    "compilerOptions": { 
        "target": "esnext", 
        "moduleResolution": "bundler" 
    } 
}

Si vous utilisez un bundler moderne comme Vite, esbuild, swc, Webpack, Parcel, et d'autres qui mettent en œuvre une stratégie de recherche hybride, la nouvelle option bundler devrait vous convenir.

Flags de personnalisation de la résolution

Les outils JavaScript peuvent désormais modéliser des règles de résolution "hybrides", comme dans le mode bundler que nous avons décrit ci-dessus. Parce que les outils peuvent différer légèrement dans leur support, TypeScript 5.0 fournit des moyens d'activer ou de désactiver quelques fonctionnalités qui peuvent ou non fonctionner avec votre configuration.

allowImportingTsExtensions

--allowImportingTsExtensions permet aux fichiers TypeScript de s'importer mutuellement avec une extension spécifique à TypeScript comme .ts, .mts ou .tsx.

Ce flag n'est autorisé que lorsque --noEmit ou --emitDeclarationOnly est activé, car ces chemins d'importation ne seraient pas résolvables au moment de l'exécution dans les fichiers de sortie JavaScript. On s'attend ici à ce que votre résolveur (par exemple votre bundler, un runtime ou un autre outil) fasse fonctionner ces importations entre fichiers .ts.

resolvePackageJsonExports

--resolvePackageJsonExports force TypeScript à consulter le champ exports des fichiers package.json s'il lit un package dans node_modules.

Cette option a la valeur true par défaut sous les options node16, nodenext, et bundler pour --moduleResolution.

resolvePackageJsonImports

--resolvePackageJsonImports force TypeScript à consulter le champ imports des fichiers package.json lorsqu'il effectue une recherche qui commence par # à partir d'un fichier dont le répertoire ancêtre contient un package.json.

Cette option a la valeur true par défaut sous les options node16, nodenext, et bundler pour --moduleResolution.

allowArbitraryExtensions

Dans TypeScript 5.0, lorsqu'un chemin d'importation se termine par une extension qui n'est pas une extension de fichier JavaScript ou TypeScript connue, le compilateur recherche un fichier de déclaration pour ce chemin sous la forme {nom de base du fichier}.d.{extension}.ts. Par exemple, si vous utilisez un chargeur CSS dans un projet bundler, vous pouvez souhaiter écrire (ou générer) des fichiers de déclaration pour ces feuilles de style :

Code : Sélectionner tout
1
2
3
4
/* app.css */ 
.cookie-banner { 
  display: none; 
}
Code : Sélectionner tout
1
2
3
4
5
// app.d.css.ts 
declare const css: { 
  cookieBanner: string; 
}; 
export default css;
Code : Sélectionner tout
1
2
3
4
// App.tsx 
import styles from "./app.css"; 
 
styles.cookieBanner; // string

Par défaut, cette importation génère une erreur pour vous informer que TypeScript ne comprend pas ce type de fichier et que votre moteur d'exécution peut ne pas prendre en charge son importation. Mais si vous avez configuré votre runtime ou bundler pour le gérer, vous pouvez supprimer l'erreur avec la nouvelle option de compilation --allowArbitraryExtensions.

Notez qu'historiquement, un effet similaire a souvent pu être obtenu en ajoutant un fichier de déclaration nommé app.css.d.ts au lieu de app.d.css.ts - cependant, cela a juste fonctionné à travers les règles de résolution require de Node pour CommonJS. Strictement parlant, le premier est interprété comme un fichier de déclaration pour un fichier JavaScript nommé app.css.js. Comme les importations de fichiers relatifs doivent inclure des extensions dans le support ESM de Node, TypeScript commettrait une erreur sur notre exemple dans un fichier ESM sous --moduleResolution node16 ou nodenext.

customConditions

--customConditions prend une liste de conditions supplémentaires qui devraient réussir lorsque TypeScript résout à partir d'un champ [exports] ou imports d'un package.json. Ces conditions sont ajoutées aux conditions existantes qu'un résolveur utilisera par défaut.

Par exemple, lorsque ce champ est défini dans un tsconfig.json comme suit :

Code : Sélectionner tout
1
2
3
4
5
6
7
{ 
    "compilerOptions": { 
        "target": "es2022", 
        "moduleResolution": "bundler", 
        "customConditions": ["my-condition"] 
    } 
}

Chaque fois qu'un champ exports ou imports est référencé dans package.json, TypeScript prendra en compte les conditions appelées my-condition.

Ainsi, lors de l'importation à partir d'un paquet avec le package.json suivant

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
{ 
    // ... 
    "exports": { 
        ".": { 
            "my-condition": "./foo.mjs", 
            "node": "./bar.mjs", 
            "import": "./baz.mjs", 
            "require": "./biz.mjs" 
        } 
    } 
}

TypeScript va essayer de rechercher les fichiers correspondant à foo.mjs.

Ce champ n'est valide que sous les options node16, nodenext, et bundler pour --moduleResolution.

--verbatimModuleSyntaxe

Par défaut, TypeScript fait quelque chose appelée élision d'importation. Fondamentalement, si vous écrivez quelque chose comme

Code : Sélectionner tout
1
2
3
4
5
import { Car } from "./car"; 
 
export function drive(car: Car) { 
    // ... 
}

TypeScript détecte que vous utilisez une importation uniquement pour les types et abandonne l'importation en entier. Votre sortie JavaScript pourrait ressembler à quelque chose comme ceci :

Code : Sélectionner tout
1
2
3
export function drive(car) { 
    // ... 
}

La plupart du temps, c'est une bonne chose, car si Car n'est pas une valeur exportée de ./car, nous obtiendrons une erreur d'exécution.

Mais cela ajoute une couche de complexité pour certains cas limites. Par exemple, remarquez qu'il n'y a pas d'instruction comme import "./car" ; - l'importation a été entièrement abandonnée. Cela fait réellement une différence pour les modules qui ont des effets de bord ou non.

La stratégie d'émission de TypeScript pour JavaScript présente également quelques autres couches de complexité - l'élision d'importation n'est pas toujours uniquement déterminée par la manière dont une importation est utilisée - elle consulte souvent la manière dont une valeur est également déclarée. Il n'est donc pas toujours évident de savoir si un code comme le suivant

Code : Sélectionner tout
export { Car } from "./car";

doit être préservé ou abandonné. Si Car est déclaré avec quelque chose comme class, alors il peut être préservé dans le fichier JavaScript résultant. Mais si Car est uniquement déclaré comme un type alias ou une interface, le fichier JavaScript ne doit pas exporter Car du tout.

Alors que TypeScript pourrait être en mesure de prendre ces décisions d'émission basées sur des informations provenant de plusieurs fichiers, tous les compilateurs ne le peuvent pas.

Le modificateur type sur les importations et les exportations aide un peu à résoudre ces situations. Nous pouvons rendre explicite le fait qu'une importation ou une exportation est uniquement utilisée pour l'analyse de type, et peut être abandonnée entièrement dans les fichiers JavaScript en utilisant le modificateur type.

Code : Sélectionner tout
1
2
3
4
5
6
// This statement can be dropped entirely in JS output 
import type * as car from "./car"; 
 
// The named import/export 'Car' can be dropped in JS output 
import { type Car } from "./car"; 
export { type Car } from "./car";

Les modificateurs type ne sont pas tout à fait utiles en eux-mêmes - par défaut, l'élision de module laissera toujours tomber les importations, et rien ne vous oblige à faire la distinction entre les importations et les exportations type et ordinaires. TypeScript dispose donc de l'indicateur --importsNotUsedAsValues pour s'assurer que vous utilisez le modificateur type, --preserveValueImports pour empêcher certains comportements d'élision de module, et --isolatedModules pour s'assurer que votre code TypeScript fonctionne sur différents compilateurs. Malheureusement, il est difficile de comprendre les détails de ces trois flags, et il existe encore quelques cas limites avec des comportements inattendus.

TypeScript 5.0 introduit une nouvelle option appelée --verbatimModuleSyntax pour simplifier la situation. Les règles sont beaucoup plus simples - toutes les importations ou exportations sans un modificateur type sont laissées en place. Tout ce qui utilise le modificateur type est entièrement abandonné.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
// Erased away entirely. 
import type { A } from "a"; 
 
// Rewritten to 'import { b } from "bcd";' 
import { b, type c, type d } from "bcd"; 
 
// Rewritten to 'import {} from "xyz";' 
import { type xyz } from "xyz";

Avec cette nouvelle option, ce que vous voyez est ce que vous obtenez.

Cela a cependant quelques implications lorsqu'il s'agit de l'interopérabilité des modules. Avec ce drapeau, les imports et exports ECMAScript ne seront pas réécrites par des appels require lorsque vos paramètres ou votre extension de fichier impliquent un système de module différent. Au lieu de cela, vous obtiendrez une erreur. Si vous devez émettre du code qui utilise require et module.exports, vous devrez utiliser la syntaxe de module de TypeScript qui est antérieure à ES2015 :


Bien qu'il s'agisse d'une limitation, cela permet de rendre certains problèmes plus évidents. Par exemple, il est très courant d'oublier de définir le champ type dans package.json sous --module node16. Par conséquent, les développeurs commencent à écrire des modules CommonJS au lieu de modules ES sans s'en rendre compte, ce qui donne des règles de recherche et des résultats JavaScript surprenants. Ce nouveau flag garantit que vous êtes intentionnel quant au type de fichier que vous utilisez car la syntaxe est intentionnellement différente.

Parce que --verbatimModuleSyntax fournit une histoire plus cohérente que --importsNotUsedAsValues et --preserveValueImports, ces deux flags existants sont dépréciés en sa faveur.

Prise en charge de export type *

Lorsque TypeScript 3.8 a introduit les importations de type uniquement, la nouvelle syntaxe n'était pas autorisée sur les réexportations export * from "module" ou export * as ns from "module". TypeScript 5.0 ajoute la prise en charge de ces deux formes :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// models/vehicles.ts 
export class Spaceship { 
  // ... 
} 
 
// models/index.ts 
export type * as vehicles from "./spaceship"; 
 
// main.ts 
import { vehicles } from "./models"; 
 
function takeASpaceship(s: vehicles.Spaceship) { 
  //  ok - `vehicles` only used in a type position 
} 
 
function makeASpaceship() { 
  return new vehicles.Spaceship(); 
  //         ^^^^^^^^ 
  // 'vehicles' cannot be used as a value because it was exported using 'export type'. 
}

Support de @satisfies dans JSDoc

TypeScript 4.9 a introduit l'opérateur satisfies. Il s'assure que le type d'une expression est compatible, sans affecter le type lui-même. Par exemple, prenons le code suivant :

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
interface CompilerOptions { 
    strict?: boolean; 
    outDir?: string; 
    // ... 
 
    extends?: string | string[]; 
} 
 
declare function resolveConfig(configPath: string): CompilerOptions; 
 
let myCompilerOptions = { 
    strict: true, 
    outDir: "../lib", 
    // ... 
 
    extends: [ 
        "@tsconfig/strictest/tsconfig.json", 
        "../../../tsconfig.base.json" 
    ], 
 
} satisfies CompilerOptions;

Ici, TypeScript sait que...
La fin de cet article est réservée aux abonnés. Soutenez le Club Developpez.com en prenant un abonnement pour que nous puissions continuer à vous proposer des publications.

Une erreur dans cette actualité ? Signalez-nous-la !