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 Release Candidate de TypeScript 5.2
Et présente une liste rapide des nouveautés de TypeScript 5.2

Le , par Nancy Rey

38PARTAGES

5  0 
Microsoft est heureux d'annoncer la Release Candidate de TypeScript 5.2 ! D'ici la sortie de la version stable de TypeScript 5.2, Microsoft ne prévoit pas d'autres changements que des corrections de bogues critiques.


Pour commencer à utiliser la RC, vous pouvez l'obtenir via NuGet, ou via npm avec la commande suivante :

Code : Sélectionner tout
npm install -D typescript@rc

Voici une liste rapide des nouveautés de TypeScript 5.2 !

  • Utilisation des déclarations et gestion explicite des ressources
  • Métadonnées des décorateurs
  • Éléments de tuple nommés et anonymes
  • Utilisation plus facile des méthodes pour les Unions de tableaux
  • Chemins d'importation réservés aux types avec extensions de fichiers d'implémentation TypeScript
  • Compléments de virgule pour les membres d'objets
  • Refonte des variables en ligne
  • Vérifications optimisées pour la compatibilité des types en cours
  • Changements de rupture et corrections de correction


Quoi de neuf depuis la version bêta ?

Depuis la version bêta, Micorsoft a ajouté une optimisation de la vérification des types et rendu possible la référence aux chemins des fichiers d'implémentation TypeScript dans les importations de types uniquement.

Using Declarations and Explicit Resource Management (utilisation 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);
}
C'est très bien, mais que se passe-t-il si une sortie anticipée doit être effectuée ?

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);
}


Microsoft commence à voir des duplications de nettoyage qui peuvent être facilement oubliées. Microsoft n’est pas non plus assuré de fermer et de supprimer le fichier en cas d'erreur. Cela pourrait être résolu en enveloppant le tout dans un bloc try/finally.

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);
    }
}
Bien que cette méthode soit plus robuste, elle a ajouté pas mal de "bruit" au code. Il y a également d'autres obstacles que Microsoft peut rencontrer si Microsoft commence à ajouter plus de logique de nettoyage à son bloc finally - par exemple, des exceptions empêchant d'autres ressources d'être éliminées. C'est ce que la proposition de gestion explicite des ressources vise à résoudre. L'idée principale de la proposition est de soutenir l'élimination des ressources - ce travail de nettoyage que nous essayons de gérer - comme une idée de première classe en JavaScript.

Cela commence par l'ajout d'un nouveau symbole intégré appelé Symbol.dispose, et Microsoft peut 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);
    }
}
Plus tard ces méthodes pourrons être appelées

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]();
    }
}


Déplacer la logique de nettoyage dans TempFile lui-même n’apporte pas grand-chose ; Microsoft a essentiellement déplacé tout le travail de nettoyage du bloc finally dans une méthode, ce qui a toujours été possible. Mais le fait d'avoir un "nom" bien connu pour cette méthode signifie que JavaScript peut construire d'autres fonctionnalités au-dessus d'elle.

Cela nous amène à la première étoile de la fonctionnalité : les using déclarations ! 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;
    }
}


Regardez : pas de try/finally blocs ! Du moins, pas à la connaissance de Microsoft. D'un point de vue fonctionnel, c'est exactement ce que les using déclarations font pour nous, mais nous n'avons pas à nous en préoccuper.

Vous connaissez peut-être les using 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é usingde JavaScript et fournissent un moyen explicite similaire d'effectuer un "nettoyage" d'un objet à la fin d'un champ d'application.

Les using déclarations effectuent ce nettoyage à la toute fin de la portée qu'elles contiennent ou juste avant un "retour anticipé" tel qu'un return ou thrownerror. Elles se débarrassent également des objets 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");
}

func();
// Creating a
// Creating b
// Creating c
// Creating d
// Disposing d
// Disposing c
// Creating e
// Disposing e
// Disposing b
// Disposing a


Les usingdéclarations sont censées être résistantes aux exceptions ; si une erreur est lancée, elle est relancée après l'élimination. D'un autre côté, le corps de votre fonction peut s'exécuter comme prévu, mais le Symbol.disposepeut être rejeté. Dans ce cas, cette exception est également relancée.

Mais que se passe-t-il si la logique avant et pendant l'élimination génère une erreur ? Pour ces cas, SuppressedErrora été introduit comme nouveau sous-type Error. Il comporte une propriété suppressedqui contient la dernière erreur levée, et une propriété errorpour 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 des méthodes synchrones sont utilisées dans ces exemples. Cependant, une grande partie de l'élimination des ressources implique des opérations asynchrones, et Microsoft doit 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 amène à la prochaine vedette du spectacle : l'utilisation des déclarations d'attente. Celles-ci sont similaires aux await usingdéclarations, mais la clé est qu'elles recherchent la disposition qui doit êtreawait. 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");
}

func();
// 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 Disposableet AsyncDisposablepeut 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.disposeet 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 Microsoft voulait, c'était de se souvenir d'appeler deux fonctions - mais était-ce la meilleure façon de l'écrire ? Faudrait-il appeler openSyncdans le constructeur, créer une méthode open ( ), ou passer la poignée nous-mêmes ? Faudrait-il exposer une méthode pour chaque opération possible à effectuer, ou Faudrait-il simplement rendre les propriétés publiques ?

Cela nous amène aux dernières étoiles de la fonctionnalité : DisposableStacket AsyncDisposableStack. Ces objets sont utiles pour effectuer à la fois un nettoyage ponctuel et des quantités arbitraires de nettoyage. Un DisposableStackest 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 usingvariables parce que - tenez-vous bien - elles sont également jetables ! Voici donc comment l'exemple original aurait pu être écrit.

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

    // ...
}
Ici, la méthode defer() prend juste un callback, et ce callback sera exécuté une fois que le cleanup aura été éliminé. Généralement, defer(et d'autres méthodes DisposableStackcomme useet adopt) doit être appelé immédiatement après la création d'une ressource. Comme son nom l'indique, DisposableStackse débarrasse de tout ce qu'elle suit comme une pile, dans l'ordre "premier entré - dernier sorti", doncdiffer immédiatement après la création d'une valeur permet d'éviter les problèmes de dépendance. AsyncDisposablefonctionne de manière similaire, mais peut garder la trace desasync fonctions et des AsyncDisposables, et est lui-même un AsyncDisposable.

La méthode deferest 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
  • Pile jetable
  • AsyncDisposableStack
  • SuppressedError


Cependant, si tout ce qui vous intéresse estusing et await using, vous devriez pouvoir vous en sortir en ne remplissant que les symboles 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");
Vous devrez également définir votre compilation targetà es2022ou moins, et configurer votre librairie pour inclure "esnext" ou "esnext.disposable"

Code : Sélectionner tout
1
2
3
4
5
6
{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "esnext.disposable", "dom"]
    }
}
Métadonnées des décorateurs

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


Cela peut être utile dans un certain nombre de scénarios différents. Les métadonnées peuvent être attachées à de nombreux usages tels que le débogage, la sérialisation ou l'injection de dépendances avec des décorateurs. Puisque les objets de métadonnées sont créés par classe décorée, les frameworks peuvent soit les utiliser en privé comme clés dans une Mapou une WeakMap, soit leur ajouter des propriétés si nécessaire.

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.stringifyde 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
import { 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) {
        // ...
    }
}
Ici, l'intention est que seuls ageet fullNamesoient sérialisés parce qu'ils sont marqués avec le décorateur @serialize. Nous définissons une méthode toJSONà cette fin, mais elle appelle simplement jsonifyqui utilise les métadonnées créées par @serialize.

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(", ")} }`;
}
Ce module possède un local symbolappelé serializablespour stocker et récupérer les noms des propriétés marquées @serializable. Il stocke une liste de ces noms de propriétés dans les métadonnées à chaque invocation de @serializable. Lorsque jsonifyest appelé, la liste des propriétés est extraite des métadonnées et utilisée pour récupérer les valeurs réelles de l'instance, puis pour sérialiser ces noms et ces valeurs.

L'utilisation d'un symbolrend techniquement ces données accessibles à d'autres personnes. Une alternative pourrait être d'utiliser un WeakMapen 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.");
    }

    let 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(", ")} }`;
}
Il est à noter que ces implémentations ne gèrent pas la sous-classe et l'héritage. C'est un exercice qui vous est laissé (et vous pourriez trouver que c'est plus facile dans une version du fichier que dans l'autre !)

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");


Vous devrez également définir votre compilation targetà es2022ou moins, et configurer votre lib pour inclure "esnext" ou "esnext.decorators".

Code : Sélectionner tout
1
2
3
4
5
6
{
    "compilerOptions": {
        "target": "es2022",
        "lib": ["es2022", "esnext.decorators", "dom"]
    }
}
Éléments de tuple nommés et anonymes

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];
Ces étiquettes ne modifient pas ce que vous êtes autorisé à faire avec eux - elles sont uniquement destinées à faciliter la lisibilité et l'outillage.

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 en avaient besoin.

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.
Cela pourrait être gênant pour les éléments "rest", pour lesquels nous serions obligés d'ajouter un libellé tel que rest ou tail.

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[]];


Cela signifie également que cette restriction doit être appliquée en interne dans le système de types, ce qui signifie que TypeScript perdrait des étiquettes.

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'
Dans TypeScript 5.2, la restriction "tout ou rien" sur les étiquettes de tuple a été levée. Le langage peut désormais également préserver les étiquettes lors de la propagation dans un tuple non étiqueté.

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 filterest 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 filterest invoqué sur ce type. Il y a un léger inconvénient : filter produira un tableau<chaîne | nombre> au lieu d'une chaîne[] | nombre[] ; 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, everyet reducedevraient toutes pouvoir être invoquées sur des unions de tableaux dans des cas où elles ne l'étaient pas auparavant.

Chemins d'importation de type uniquement avec les extensions de fichiers d'implémentation TypeScript

TypeScript permet désormais aux extensions de fichiers de déclaration et d'implémentation d'être incluses dans les chemins d'importation de type uniquement, que l'option allowImportingTsExtensionssoit activée ou non.

Cela signifie que vous pouvez désormais écrire des instructions import typequi utilisent les extensions de fichiers .ts, .mts, .cts et .tsx.

Code : Sélectionner tout
1
2
3
4
5
import type { JustAType } from "./justTypes.ts";

export function f(param: JustAType) {
    // ...
}
Cela signifie également que les types import(), qui peuvent être utilisés à la fois en TypeScript et en JavaScript avec JSDoc, peuvent utiliser ces extensions de fichier.

Code : Sélectionner tout
1
2
3
4
5
6
/**
 * @param {import("./justTypes.ts").JustAType} param
 */
export function f(param) {
    // ...
}
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 gracieuse 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.

Vérifications optimisées pour la compatibilité continue des types

TypeScript étant un système de types structurels, les types doivent parfois être comparés par membre. Cependant, les types récursifs posent quelques problèmes :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
interface A {
    value: A;
    other: string;
}

interface B {
    value: B;
    other: number;
}
En vérifiant si le type Aest compatible avec le type B, TypeScript finira par vérifier si les types de valuede Aet Bsont respectivement compatibles. À ce stade, le système de type doit cesser de vérifier et passer à la vérification d'autres membres. Pour ce faire, le système de types doit savoir quand deux types sont déjà liés.

Auparavant, TypeScript conservait déjà une pile de paires de types et la parcourait pour déterminer si ces types étaient liés. Lorsque cette pile est peu profonde, ce n'est pas un problème ; mais lorsque la pile n'est pas peu profonde, c'est, euh, un problème.

Dans TypeScript 5.3, un simple Setpermet de suivre ces informations. Cela a permis de réduire de plus de 33 % le temps passé sur un cas de test signalé qui utilisait la bibliothèque drizzle !

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
Benchmark 1: old
  Time (mean ± &#963;):      3.115 s ±  0.067 s    [User: 4.403 s, System: 0.124 s]
  Range (min … max):    3.018 s …  3.196 s    10 runs
 
Benchmark 2: new
  Time (mean ± &#963;):      2.072 s ±  0.050 s    [User: 3.355 s, System: 0.135 s]
  Range (min … max):    1.985 s …  2.150 s    10 runs
 
Summary
  'new' ran
    1.50 ± 0.05 times faster than 'old'
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. Pour plus d'informations, voir les mises à jour DOM pour TypeScript 5.2.

labeledElementDeclarationspeut 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é labeledElementDeclarationsde TupleTypepeut contenir des éléments undefined à chaque position où un élément n'est pas étiqueté.

Code : Sélectionner tout
1
2
3
4
  interface TupleType {
-     labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration)[];
+     labeledElementDeclarations?: readonly (NamedTupleMember | ParameterDeclaration | undefined)[];
  }
moduleet moduleResolutiondoivent correspondre aux paramètres récents de Node.js

Les options --module et --moduleResolution supportent chacune les paramètres node16et 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 node16ou nodenextpour 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'.


ou

Code : Sélectionner tout
Option 'module' must be set to 'Node16' when option 'moduleResolution' is set to 'Node16'.
Ainsi, par exemple, --module esnext --moduleResolution node16 sera rejeté, mais il est préférable d'utiliser --module nodenext seul, ou --module esnext --moduleResolution bundler.

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 de declare module. Par exemple, il n'émettrait pas d'erreur dans un cas comme celui-ci, où replaceInFileest 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 replaceInFiledoivent ê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.
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.2 Beta et présente une liste rapide des nouveautés de TypeScript 5.2

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

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