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

Le , par Nancy Rey

31PARTAGES

5  0 
Microsoft est heureux d'annoncer la disponibilité de TypeScript 5.2 Beta. Pour commencer à utiliser la version bêta, vous pouvez l'obtenir via NuGet, ou via npm avec la commande suivante :

Code : Sélectionner tout
npm install -D typescript@beta
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 "Tuple" nommés et anonymes
  • Utilisation plus facile des méthodes pour les Unions de tableaux
  • Compléments de virgule pour les membres d'objets
  • Refonte des variables en ligne
  • Changements de rupture et corrections d'erreurs



using des déclarations et de la gestion explicite des ressources

TypeScript 5.2 prend en charge la prochaine fonctionnalité de gestion explicite des ressources (Explicit Resource Management) de l'ECMAScript. Explorons quelques-unes des motivations et comprenons ce que cette fonctionnalité nous apporte.

Il est fréquent de devoir effectuer une sorte de "nettoyage" après la création d'un objet. Par exemple, vous pouvez avoir besoin de fermer des connexions réseau, de supprimer des fichiers temporaires ou simplement de libérer de la mémoire.

Imaginons une fonction qui crée un fichier temporaire, y lit et y écrit pour diverses opérations, puis le ferme et le supprime.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
import * as fs from "fs";

export function doSomeWork() {
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");

    // use file...

    // Close the file and delete it.
    fs.closeSync(file);
    fs.unlinkSync(path);
}
C'est très bien, mais que se passe-t-il si nous devons effectuer une sortie anticipé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);
}
Nous commençons à voir des duplications de nettoyage qui peuvent être facilement oubliées. Nous ne sommes pas non plus assurés 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" à notre code. Il y a également d'autres obstacles que nous pouvons rencontrer si nous commençons à ajouter plus de logique de nettoyage à notre 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 Symbol intégré appelé Symbol.dispose, et nous pouvons créer des objets avec des méthodes nommées par Symbol.dispose. Pour plus de commodité, TypeScript définit un nouveau type global appelé Disposable qui décrit ces objets.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class TempFile implements Disposable {
    #path: string;
    #handle: number;

    constructor(path: string) {
        this.#path = path;
        this.#handle = fs.openSync(path, "w+");
    }

    // other methods

    [Symbol.dispose]() {
        // Close the file and delete it.
        fs.closeSync(this.#handle);
        fs.unlinkSync(this.#path);
    }
}
Plus tard, nous pourrons appeler ces méthodes.

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 ne nous apporte pas grand-chose ; nous avons 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 déclarations using ! using est un nouveau mot-clé qui nous permet de déclarer de nouvelles liaisons fixes, un peu comme const. La principale différence est que les variables déclarées avec using voient leur méthode Symbol.dispose appelée à la fin de la portée !

Nous aurions donc pu simplement écrire notre code comme suit :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
export function doSomeWork() {
    using file = new TempFile(".some_temp_file");

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}
Regardez : pas de blocs try/finally ! Du moins, pas à notre connaissance. D'un point de vue fonctionnel, c'est exactement ce que les déclarations font pour nous, mais nous n'avons pas à nous en préoccuper.

Vous connaissez peut-être les déclarations en C#, les instructions en Python ou les déclarations try-with-resource en Java. Ces déclarations sont toutes similaires au nouveau mot-clé using de JavaScript et fournissent un moyen explicite similaire d'effectuer un "nettoyage" d'un objet à la fin d'un champ d'application.

Les déclarations using effectuent ce nettoyage à la toute fin de la portée qu'elles contiennent ou juste avant un "retour anticipé" tel qu'un return ou un thrown error. Elles se débarrassent également de l'objet dans l'ordre premier entré-dernier sorti, comme une pile.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
function loggy(id: string): Disposable {
    console.log(`Creating ${id}`);

    return {
        [Symbol.dispose]() {
            console.log(`Disposing ${id}`);
        }
    }
}

function func() {
    using a = loggy("a");
    using b = loggy("b");
    {
        using c = loggy("c");
        using d = loggy("d");
    }
    using e = loggy("e");
    return;

    // Unreachable.
    // Never created, never disposed.
    using f = loggy("f");
}

f();
// Creating a
// Creating b
// Creating c
// Creating d
// Disposing d
// Disposing c
// Creating e
// Disposing e
// Disposing b
// Disposing a
Les déclarations using 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.dispose peut ê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, SuppressedError a été introduit comme nouveau sous-type de Error. Il comporte une propriété suppressed qui contient la dernière erreur levée, et une propriété error pour l'erreur la plus récente.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ErrorA extends Error {
    name = "ErrorA";
}
class ErrorB extends Error {
    name = "ErrorB";
}

function throwy(id: string) {
    return {
        [Symbol.dispose]() {
            throw new ErrorA(`Error from ${id}`);
        }
    };
}

function func() {
    using a = throwy("a");
    throw new ErrorB("oops!")
}

try {
    func();
}
catch (e: any) {
    console.log(e.name); // SuppressedError
    console.log(e.message); // An error was suppressed during disposal.

    console.log(e.error.name); // ErrorA
    console.log(e.error.message); // Error from a

    console.log(e.suppressed.name); // ErrorB
    console.log(e.suppressed.message); // oops!
}


Vous avez peut-être remarqué que nous utilisons des méthodes synchrones dans ces exemples. Cependant, une grande partie de l'élimination des ressources implique des opérations asynchrones, et nous devons attendre qu'elles soient terminées avant de continuer à exécuter tout autre code.

C'est pourquoi il existe également un nouveau Symbol.asyncDispose, qui nous amène à la prochaine vedette du spectacle — les déclarations await using. Celles-ci sont similaires aux déclarations d'utilisation, mais la clé est qu'elles recherchent la disposition qui doit await. Elles utilisent une méthode différente nommée Symbol.asyncDispose, bien qu'elles puissent également opérer sur tout ce qui possède un Symbol.dispose. Par commodité, TypeScript introduit également un type global appelé AsyncDisposable qui décrit tout objet doté d'une méthode d'élimination asynchrone.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
async function doWork() {
    // Do fake work for half a second.
    await new Promise(resolve => setTimeout(resolve, 500));
}

function loggy(id: string): AsyncDisposable {
    console.log(`Constructing ${id}`);
    return {
        async [Symbol.asyncDispose]() {
            console.log(`Disposing (async) ${id}`);
            await doWork();
        },
    }
}

async function func() {
    await using a = loggy("a");
    await using b = loggy("b");
    {
        await using c = loggy("c");
        await using d = loggy("d");
    }
    await using e = loggy("e");
    return;

    // Unreachable.
    // Never created, never disposed.
    await using f = loggy("f");
}

f();
// Constructing a
// Constructing b
// Constructing c
// Constructing d
// Disposing (async) d
// Disposing (async) c
// Constructing e
// Disposing (async) e
// Disposing (async) b
// Disposing (async) a


Définir les types en termes de Disposable et AsyncDisposable peut rendre votre code beaucoup plus facile à travailler si vous attendez des autres qu'ils fassent de la logique de destruction de manière cohérente. En fait, il existe de nombreux types existants dans la nature qui ont une méthode dispose( ) ou close( ). Par exemple, les API de Visual Studio Code définissent même leur propre interface Disposable. Les API dans le navigateur et dans les moteurs d'exécution comme Node.js, Deno et Bun peuvent également choisir d'utiliser Symbol.dispose et Symbol.asyncDispose pour les objets qui ont déjà des méthodes de nettoyage, comme les poignées de fichiers, les connexions, et plus encore.

Peut-être que tout cela semble excellent pour les bibliothèques, mais un peu lourd pour vos scénarios. Si vous faites beaucoup de nettoyage ad-hoc, la création d'un nouveau type pourrait introduire beaucoup de sur-abstraction et des questions sur les meilleures pratiques. Reprenons l'exemple de TempFile.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class TempFile implements Disposable {
    #path: string;
    #handle: number;

    constructor(path: string) {
        this.#path = path;
        this.#handle = fs.openSync(path, "w+");
    }

    // other methods

    [Symbol.dispose]() {
        // Close the file and delete it.
        fs.closeSync(this.#handle);
        fs.unlinkSync(this.#path);
    }
}

export function doSomeWork() {
    using file = new TempFile(".some_temp_file");

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }
}


Tout ce que nous voulions, c'était nous souvenir d'appeler deux fonctions - mais était-ce la meilleure façon de l'écrire ? Devrions-nous appeler openSync dans le constructeur, créer une méthode open( ), ou passer la poignée nous-mêmes ? Devrions-nous exposer une méthode pour chaque opération possible que nous devons effectuer, ou devrions-nous simplement rendre les propriétés publiques ?

Cela nous amène aux dernières étoiles de la fonctionnalité : DisposableStack et AsyncDisposableStack. Ces objets sont utiles pour effectuer à la fois un nettoyage ponctuel et des quantités arbitraires de nettoyage. Un DisposableStack est un objet qui possède plusieurs méthodes pour garder une trace des objets Disposable, et qui peut recevoir des fonctions pour effectuer un travail de nettoyage arbitraire. Nous pouvons également les assigner à des variables using parce que - tenez-vous bien - elles sont également Disposable ! Voici donc comment nous aurions pu écrire l'exemple original.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function doSomeWork() {
    const path = ".some_temp_file";
    const file = fs.openSync(path, "w+");

    using cleanup = new DisposableStack();
    cleanup.defer(() => {
        fs.closeSync(file);
        fs.unlinkSync(path);
    });

    // use file...

    if (someCondition()) {
        // do some more work...
        return;
    }

    // ...
}
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 DisposableStack comme use et adopt) doit être appelé immédiatement après la création d'une ressource. Comme son nom l'indique, DisposableStack se débarrasse de tout ce qu'elle suit comme une pile, dans l'ordre "premier entré - dernier sorti", donc difer immédiatement après la création d'une valeur permet d'éviter les problèmes de dépendance. AsyncDisposable fonctionne de manière similaire, mais peut garder la trace des fonctions async et des AsyncDisposables, et est lui-même un AsyncDisposable.

La méthode defer est similaire à bien des égards au mot-clé defer dans Go, Swift, Zig, Odin, et d'autres, où les conventions devraient être similaires.

Cette fonctionnalité étant très récente, la plupart des systèmes d'exécution ne la supporteront pas nativement. Pour l'utiliser, vous aurez besoin de polyfills pour les éléments suivants :

  • Symbol.dispose
  • Symbol.asyncDispose
  • DisposableStack
  • AsyncDisposableStack
  • SuppressedError


Cependant, si tout ce qui vous intéresse est using et await using, vous devriez pouvoir vous en sortir en ne remplissant que les symbols intégrés. Quelque chose d'aussi simple que ce qui suit devrait fonctionner dans la plupart des cas :

Code : Sélectionner tout
1
2
Symbol.dispose ??= Symbol("Symbol.dispose");
Symbol.asyncDispose ??= Symbol("Symbol.asyncDispose");
Vous devrez également définir votre compilation target à es2022 ou moins, et configurer votre lib 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 portion décorée d'une classe, il peut être utilisé comme une clé dans une Map. Une fois que tous les décorateurs sur ou dans une classe ont été exécutés, cet objet peut être accédé sur la classe via Symbol.metadata.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
interface Context {
    name: string;
    metadata: Record<PropertyKey, unknown>;
}

function setMetadata(_target: any, context: Context) {
    context.metadata[context.name] = true;
}

class SomeClass {
    @setMetadata
    foo = 123;

    @setMetadata
    accessor bar = "hello!";

    @setMetadata
    baz() { }
}

const ourMetadata = SomeClass[Symbol.metadata];

console.log(JSON.stringify(ourMetadata));
// { "bar": true, "baz": true, "foo": true }
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 Map ou 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.stringify de la manière suivante :

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
mport { serialize, jsonify } from "./serializer";

class Person {
    firstName: string;
    lastName: string;

    @serialize
    age: number

    @serialize
    get fullName() {
        return `${this.firstName} ${this.lastName}`;
    }

    toJSON() {
        return jsonify(this)
    }

    constructor(firstName: string, lastName: string, age: number) {
        // ...
    }
}
Ici, l'intention est que seuls age et fullName soient 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 jsonify qui 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 symbol local appelé serializables pour 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 jsonify est 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 symbole rend techniquement ces données accessibles à d'autres personnes. Une alternative pourrait être d'utiliser un WeakMap en utilisant l'objet de métadonnées comme clé. Cela permet de garder les données privées et d'utiliser moins d'assertions de type dans ce cas, mais c'est autrement similaire.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
const serializables = new WeakMap<object, string[]>();

type Context =
    | ClassAccessorDecoratorContext
    | ClassGetterDecoratorContext
    | ClassFieldDecoratorContext
    ;

export function serialize(_target: any, context: Context): void {
    if (context.static || context.private) {
        throw new Error("Can only serialize public instance members.")
    }
    if (typeof context.name !== "string") {
        throw new Error("Can only serialize string properties.");
    }

    const propNames = serializables.get(context.metadata);
    if (propNames === undefined) {
        serializables.set(context.metadata, propNames = []);
    }
    propNames.push(context.name);
}

export function jsonify(instance: object): string {
    const metadata = instance.constructor[Symbol.metadata];
    const propNames = metadata && serializables.get(metadata);
    if (!propNames) {
        throw new Error("No members marked with @serialize.");
    }
    const pairStrings = propNames.map(key => {
        const strKey = JSON.stringify(key);
        const strValue = JSON.stringify((instance as any)[key]);
        return `${strKey}: ${strValue}`;
    });

    return `{ ${pairStrings.join(", ")} }`;
}
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 à es2022 ou 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 devaient en avoir une.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
//  fine - no labels
type Pair1<T> = [T, T];

//  fine - all fully labeled
type Pair2<T> = [first: T, second: T];

//  previously an error
type Pair3<T> = [first: T, T];
//                         ~
// Tuple members must all have names
// or all not have names.
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 de filter est compatible avec string[ ] et number[ ]. Sans stratégie cohérente, TypeScript a jeté ses mains en l'air et a dit "je ne peux pas le faire fonctionner".

Dans TypeScript 5.2, avant d'abandonner dans ces cas, les unions de tableaux sont traitées comme un cas spécial. Un nouveau type de tableau est construit à partir du type d'élément de chaque membre, puis la méthode est invoquée sur celui-ci.

Dans l'exemple ci-dessus, string[ ] | number[ ] est transformé en (string | number)[ ] (ou Array<string | number>, et filter est invoqué sur ce type. Il y a un léger inconvénient : filter produira un Array<string | number> au lieu d'un string[] | number[] ; mais pour une valeur fraîchement produite, il y a moins de risque que quelque chose "tourne mal".

Cela signifie que de nombreuses méthodes telles que filter, find, some, every et reduce devraient toutes pouvoir être invoquées sur des unions de tableaux dans des cas où elles ne l'étaient pas auparavant.

Compléments à la virgule pour les membres d'un objet

Il peut être facile d'oublier d'ajouter une virgule lors de l'ajout d'une nouvelle propriété à un objet. Auparavant, si vous oubliiez une virgule et que vous demandiez l'auto-complétion, TypeScript donnait des résultats de complétion médiocres et sans rapport avec le sujet.

TypeScript 5.2 fournit désormais de manière élégante des compléments sur les membres d'un objet lorsqu'il manque une virgule. Mais pour éviter de vous infliger une erreur de syntaxe, TypeScript insère automatiquement la virgule manquante.


Refactorisation des variables en ligne

TypeScript 5.2 dispose désormais d'un refactoring permettant d'intégrer le contenu d'une variable dans tous les sites d'utilisation.


L'utilisation du refactoring "inline variable" éliminera la variable et remplacera toutes les utilisations de la variable par son initialisateur. Notez que cela peut entraîner l'exécution des effets secondaires de l'initialisateur à un moment différent et autant de fois que la variable a été utilisée.

Changements de rupture et corrections

TypeScript s'efforce de ne pas introduire inutilement des ruptures ; cependant, nous devons parfois apporter des corrections et des améliorations afin que le code puisse être mieux analysé

Changements dans lib.d.ts

Les types générés pour le DOM peuvent avoir un impact sur votre base de code.

labeledElementDeclarations peut contenir des éléments undefined

Afin de supporter un mélange d'éléments étiquetés et non étiquetés, l'API de TypeScript a légèrement changé. La propriété labeledElementDeclarations de TupleType peut contenir des éléments non définis à chaque position où un élément n'est pas étiqueté.

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

Les options --module et --moduleResolution supportent chacune les paramètres node16 et nodenext. Il s'agit en fait de paramètres "Node.js modernes" qui devraient être utilisés dans tout projet Node.js récent. Nous avons constaté que lorsque ces deux options ne s'accordent pas sur l'utilisation des paramètres liés à Node.js, les projets sont effectivement mal configurés.

Dans TypeScript 5.2, lorsque l'on utilise node16 ou nodenex pour les options --module et --moduleResolution, TypeScript exige désormais que l'autre option ait un paramètre similaire lié à Node.js. Dans les cas où les paramètres divergent, vous obtiendrez probablement un message d'erreur du type.

Code : Sélectionner tout
Option 'moduleResolution' must be set to 'NodeNext' (or left unspecified) when option 'module' is set to 'NodeNext'.
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 declare module. Par exemple, il n'émettrait pas d'erreur dans un cas comme celui-ci, où replaceInFile est déclaré une fois en tant que fonction exportée, et une fois en tant qu'espace de noms non exporté.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
declare module 'replace-in-file' {
    export function replaceInFile(config: unknown): Promise<unknown[]>;
    export {};

    namespace replaceInFile {
        export function sync(config: unknown): unknown[];
  }
}


Dans un module ambiant, l'ajout d'un export { ... } ou une construction similaire comme export default ... modifie implicitement si toutes les déclarations sont automatiquement exportées. TypeScript reconnaît maintenant cette sémantique malheureusement déroutante de manière plus cohérente, et émet une erreur sur le fait que toutes les déclarations de replaceInFile doivent être en accord dans leurs modificateurs, et émettra l'erreur suivante :

Code : Sélectionner tout
Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.
Quelles sont les prochaines étapes ?

À ce stade, TypeScript 5.2 est ce que nous appellerions "stable en termes de fonctionnalités". TypeScript 5.2 se concentrera sur les corrections de bogues, le polissage et certaines fonctionnalités d'édition à faible risque. Une release candidate sera disponible dans un peu plus d'un mois, suivie d'une version stable peu après. Si vous souhaitez planifier la sortie de cette version, n'oubliez pas de garder un œil sur le plan d'itération qui indique les dates de sortie et bien d'autres choses encore.

Remarque : bien que la version bêta soit un excellent moyen d'essayer la prochaine version de TypeScript, vous pouvez également essayer une version nocturne pour obtenir la version la plus récente de TypeScript 5.2 jusqu'à la publication de la release candidate. Les nightlies sont bien testées et peuvent même être testées uniquement dans votre éditeur.

Joyeux hacking !

- Daniel Rosenwasser et l'équipe TypeScript

Source : Microsoft

Et vous ?

Quel est votre avis sur le sujet ?

Que pensez-vous des fonctionnalités proposées par cette version de TypeScript ?

Voir aussi :

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

Microsoft annonce la disponibilité de la version 5.1 de TypeScript, et présente les changements depuis les versions bêta et release candidate

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

Avatar de melka one
Membre expérimenté https://www.developpez.com
Le 05/07/2023 à 21:17
Quel est votre avis sur le sujet ?
apparemment il faut avoir beaucoup d'imagination.
0  0