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 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

Le , par Anthony

294PARTAGES

9  0 
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...
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 !

Avatar de richardc
Membre à l'essai https://www.developpez.com
Le 01/02/2023 à 8:56
Bel article qui au moins ne reprend char après char le post original.

Merci.
1  0