TypeScript 5.0 a été officiellement publié le 16 mars 2023, et est maintenant disponible pour tout le monde. Cette version introduit de nombreuses nouvelles fonctionnalités dans le but de rendre TypeScript plus petit, plus simple et plus rapide.

Cette nouvelle version modernise les décorateurs pour la personnalisation des classes, ce qui permet de personnaliser les classes et leurs membres de manière réutilisable. Les développeurs peuvent désormais ajouter un modificateur const à la déclaration d’un paramètre de type, ce qui permet aux inférences de type const d’être utilisées par défaut. La nouvelle version rend également tous les enums union enums, ce qui simplifie la structure du code et accélère l’expérience TypeScript.

Dans cet article, vous explorerez les changements introduits dans TypeScript 5.0, en donnant un aperçu approfondi de ses nouvelles fonctionnalités et capacités.

Démarrer avec TypeScript 5.0

TypeScript est un compilateur officiel que vous pouvez installer dans votre projet à l’aide de npm. Si vous souhaitez commencer à utiliser TypeScript 5.0 dans votre projet, vous pouvez exécuter la commande suivante dans le répertoire de votre projet :

npm install -D typescript

Cela installera le compilateur dans le répertoire node_modules, que vous pouvez maintenant exécuter avec la commande npx tsc.

Vous trouverez également des instructions sur l’utilisation de la nouvelle version de TypeScript dans Visual Studio Code dans cette documentation.

Quelles sont les nouveautés de TypeScript 5.0 ?

Dans cet article, nous allons explorer 5 mises à jour majeures introduites dans TypeScript. Ces fonctionnalités sont les suivantes :

Décorateurs modernisés

Les décorateurs existent dans TypeScript depuis un certain temps sous un drapeau expérimental, mais la nouvelle version les met à jour avec la proposition ECMAScript, qui est maintenant à l’étape 3, ce qui signifie qu’elle est à un stade où elle est ajoutée à TypeScript.

Les décorateurs sont un moyen de personnaliser le comportement des classes et de leurs membres de manière réutilisable. Par exemple, si vous avez une classe qui a deux méthodes, greet et getAge:

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }

    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

Dans des cas d’utilisation réels, cette classe devrait avoir des méthodes plus compliquées qui gèrent une logique asynchrone et ont des effets secondaires, e.t.c., où vous voudriez ajouter des appels console.log pour aider à déboguer les méthodes.

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }

    greet() {
        console.log('LOG: Method Execution Starts.');
        console.log(`Hello, my name is ${this.name}.`);
        console.log('LOG: Method Execution Ends.');
    }

    getAge() {
        console.log('LOG: Method Execution Starts.');
        console.log(`I am ${this.age} years old.`);
        console.log('Method Execution Ends.');
    }
}

const p = new Person('Ron', 30);
p.greet();
p.getAge();

Il s’agit d’un schéma fréquent, et il serait pratique d’avoir une solution à appliquer à chaque méthode.

C’est là que les décorateurs entrent en jeu. Nous pouvons définir une fonction nommée debugMethod qui se présente comme suit :

function debugMethod(originalMethod: any, context: any) {
    function replacementMethod(this: any, ...args: any[]) {
        console.log('Method Execution Starts.');
        const result = originalMethod.call(this, ...args);
        console.log('Method Execution Ends.');
        return result;
    }
    return replacementMethod;
}

Dans le code ci-dessus, debugMethod prend la méthode originale (originalMethod) et renvoie une fonction qui fait ce qui suit :

  1. Elle enregistre un message « Method Execution Starts ».
  2. Elle transmet la méthode originale et tous ses arguments (y compris ceci).
  3. Enregistre un message « Method Execution Ends ».
  4. Retourne ce que la méthode originale a retourné.

En utilisant des décorateurs, vous pouvez appliquer la méthode debugMethod à vos méthodes, comme le montre le code ci-dessous :

class Person {
    name: string;
    age: number;
    constructor(name: string, age: number) {
        this.name = name;
        this.age = age;
    }
    @debugMethod
    greet() {
        console.log(`Hello, my name is ${this.name}.`);
    }
    @debugMethod
    getAge() {
        console.log(`I am ${this.age} years old.`);
    }
}
const p = new Person('Ron', 30);
p.greet();
p.getAge();

Vous obtiendrez le résultat suivant :

LOG: Entering method.
Hello, my name is Ron.
LOG: Exiting method.
LOG: Entering method.
I am 30 years old.
LOG: Exiting method.

Lors de la définition de la fonction décoratrice (debugMethod), un second paramètre est passé, appelé context (il s’agit de l’objet contextuel, qui contient des informations utiles sur la manière dont la méthode décorée a été déclarée, ainsi que le nom de la méthode). Vous pouvez mettre à jour votre debugMethod pour obtenir le nom de la méthode à partir de l’objet context:

function debugMethod(
    originalMethod: any,
    context: ClassMethodDecoratorContext
) {
    const methodName = String(context.name);
    function replacementMethod(this: any, ...args: any[]) {
        console.log(`'${methodName}' Execution Starts.`);
        const result = originalMethod.call(this, ...args);
        console.log(`'${methodName}' Execution Ends.`);
        return result;
    }
    return replacementMethod;
}

Lorsque vous exécutez votre code, la sortie contiendra désormais le nom de chaque méthode décorée avec le décorateur debugMethod:

'greet' Execution Starts.
Hello, my name is Ron.
'greet' Execution Ends.
'getAge' Execution Starts.
I am 30 years old.
'getAge' Execution Ends.

Il y a plus à faire avec les décorateurs. N’hésitez pas à consulter la pull request originale pour plus d’informations sur l’utilisation des décorateurs en TypeScript.

Introduction des paramètres de type const

Il s’agit d’une autre version importante qui vous donne un nouvel outil avec les génériques afin d’améliorer l’inférence que vous obtenez lorsque vous appelez des fonctions. Par défaut, lorsque vous déclarez des valeurs avec const, TypeScript déduit le type et non ses valeurs littérales :

// Inferred type: string[]
const names = ['John', 'Jake', 'Jack'];

Jusqu’à présent, pour obtenir l’inférence souhaitée, vous deviez utiliser l’assertion const en ajoutant « as const » :

// Inferred type: readonly ["John", "Jake", "Jack"]
const names = ['John', 'Jake', 'Jack'] as const;

Lorsque vous appelez des fonctions, c’est la même chose. Dans le code ci-dessous, le type de countries déduit est string[]:

type HasCountries = { countries: readonly string[] };
function getCountriesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: string[]
const countries = getCountriesExactly({ countries: ['USA', 'Canada', 'India'] });

Il se peut que vous souhaitiez un type plus spécifique, ce que l’on peut faire jusqu’à présent en ajoutant l’assertion as const:

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] } as const);

Cela peut être difficile à retenir et à mettre en œuvre. Cependant, TypeScript 5.0 introduit une nouvelle fonctionnalité qui vous permet d’ajouter un modificateur const à une déclaration de paramètre de type, ce qui appliquera automatiquement une inférence de type const par défaut.

type HasCountries = { countries: readonly string[] };
function getNamesExactly(arg: T): T['countries'] {
    return arg.countries;
}

// Inferred type: readonly ["USA", "Canada", "India"]
const names = getNamesExactly({ countries: ['USA', 'Canada', 'India'] });

L’utilisation des paramètres de type const permet aux développeurs d’exprimer plus clairement leur intention dans leur code. Si une variable est destinée à être constante et à ne jamais changer, l’utilisation d’un paramètre de type const garantit qu’elle ne pourra jamais être modifiée accidentellement.

Vous pouvez consulter la pull request originale pour plus d’informations sur la façon dont le paramètre de type const fonctionne dans TypeScript.

Améliorations des Enums

Les Enums dans TypeScript sont une construction puissante qui permet aux développeurs de définir un ensemble de constantes nommées. Dans TypeScript 5.0, des améliorations ont été apportées aux enums pour les rendre encore plus flexibles et utiles.

Par exemple, si vous avez l’enum suivant passé dans une fonction :

enum Color {
    Red,
    Green,
    Blue,
}

function getColorName(colorLevel: Color) {
    return colorLevel;
}

console.log(getColorName(1));

Avant l’introduction de TypeScript 5.0, vous pouviez passer un mauvais numéro de niveau, et il n’y avait pas d’erreur. Mais avec l’introduction de TypeScript 5.0, il y aura immédiatement une erreur.

De plus, la nouvelle version transforme tous les enums en unions en créant un type unique pour chaque membre calculé. Cette amélioration permet de restreindre tous les enums et de référencer leurs membres en tant que types :

enum Color {
    Red,
    Purple,
    Orange,
    Green,
    Blue,
    Black,
    White,
}

type PrimaryColor = Color.Red | Color.Green | Color.Blue;

function isPrimaryColor(c: Color): c is PrimaryColor {
    return c === Color.Red || c === Color.Green || c === Color.Blue;
}

console.log(isPrimaryColor(Color.White)); // Outputs: false
console.log(isPrimaryColor(Color.Red)); // Outputs: true

Améliorations des performances de TypeScript 5.0

TypeScript 5.0 comprend de nombreux changements significatifs dans la structure du code, les structures de données et les extensions algorithmiques. Cela a permis d’améliorer l’ensemble de l’expérience TypeScript, de l’installation à l’exécution, en la rendant plus rapide et plus efficace.

Par exemple, la différence entre la taille des paquets TypeScript 5.0 et 4.9 est assez impressionnante.

TypeScript a récemment migré des espaces de noms vers les modules, ce qui lui permet de tirer parti d’outils de construction modernes capables d’effectuer des optimisations telles que le « scope hoisting ». De plus, la suppression de certains codes obsolètes a permis de réduire de 26,4 Mo la taille du package de TypeScript 4.9, qui était de 63,8 Mo.

Taille du paquet TypeScript
Taille du paquet TypeScript

Voici quelques autres gains intéressants en termes de vitesse et de taille entre TypeScript 5.0 et 4.9 :

Scénario Temps ou taille par rapport à TS 4.9
Temps de construction de l’interface matérielle 90 %
Temps de démarrage du compilateur TypeScript 89 %
Temps de construction de Playwright 88 %
Temps d’auto-construction du compilateur TypeScript 87 %
Temps de construction d’Outlook Web 82 %
Temps de construction de VS Code 80 %
Taille du paquet typescript npm 59 %

Résolution du Bundler pour une meilleure résolution des modules

Lorsque vous écrivez une instruction d’importation en TypeScript, le compilateur doit savoir à quoi l’importation fait référence. Pour ce faire, il utilise la résolution de module. Par exemple, lorsque vous écrivez import { a } from "moduleA", le compilateur doit connaître la définition de a dans moduleA pour vérifier son utilisation.

Dans TypeScript 4.7, deux nouvelles options ont été ajoutées pour les paramètres --module et moduleResolution: node16 et nodenext.

L’objectif de ces options était de représenter plus précisément les règles de recherche exactes pour les modules ECMAScript dans Node.js. Cependant, ce mode comporte plusieurs restrictions qui ne sont pas appliquées par d’autres outils.

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

import * as utils from "./utils"; // Wrong 

import * as utils from "./utils.mjs"; // Correct

TypeScript a introduit une nouvelle stratégie appelée « moduleResolution bundler » Cette stratégie peut être mise en œuvre en ajoutant le code suivant dans la section « compilerOptions » de votre fichier de configuration TypeScript :

{
    "compilerOptions": {
        "target": "esnext",
        "moduleResolution": "bundler"
    }
}

Cette nouvelle stratégie convient à ceux qui utilisent des bundlers modernes tels que Vite, esbuild, swc, Webpack, Parcel, et d’autres qui utilisent une stratégie de recherche hybride.

Vous pouvez consulter la pull request originale et son implémentation pour plus d’informations sur la façon dont moduleResolution bundler fonctionne dans TypeScript.

Dépréciations

TypeScript 5.0 est accompagné de quelques dépréciations, y compris les exigences d’exécution, les changements de lib.d.ts, et les changements de rupture de l’API.

  1. Exigences d’exécution : TypeScript cible désormais ECMAScript 2018, et le paquetage fixe une attente minimale de moteur de 12.20. Par conséquent, les utilisateurs de Node.js doivent avoir une version minimale de 12.20 ou plus récente pour utiliser TypeScript 5.0.
  2. Changements dans lib.d.ts : Il y a eu quelques changements dans la façon dont les types pour le DOM sont générés, ce qui peut affecter le code existant. En particulier, certaines propriétés ont été converties de types numériques en types littéraux numériques, et les propriétés et méthodes pour la gestion des événements couper, copier et coller ont été déplacées à travers les interfaces.
  3. Changements de rupture dans l’API : Certaines interfaces inutiles ont été supprimées et des améliorations ont été apportées à la correction. TypeScript 5.0 est également passé aux modules.

TypeScript 5.0 a déprécié certains paramètres et leurs valeurs correspondantes, notamment target: ES3, out, noImplicitUseStrict, keyofStringsOnly, suppressExcessPropertyErrors, suppressImplicitAnyIndexErrors, noStrictGenericChecks, charset, importsNotUsedAsValues, et preserveValueImports, ainsi que prepend dans les références de projet.

Bien que ces configurations restent valables jusqu’à TypeScript 5.5, un avertissement sera émis pour alerter les utilisateurs qui les utilisent encore.

Résumé

Dans cet article, vous avez découvert quelques-unes des principales fonctionnalités et améliorations apportées par TypeScript 5.0, telles que les améliorations apportées aux enums, à la résolution des bundlers et aux paramètres de type const, ainsi que les améliorations apportées à la vitesse et à la taille.

Si vous envisagez d’utiliser TypeScript pour vos prochains projets, essayez gratuitement l’hébergement d’applications de Kinsta.

À vous de jouer ! Quelles sont les fonctionnalités ou les améliorations que vous trouvez les plus intéressantes dans TypeScript 5.0 ? Y en a-t-il d’importantes que nous n’avons pas remarquées ? Faites-nous en part dans les commentaires.

Joel Olawanle Kinsta

Joel est un développeur d'interfaces publiques qui travaille chez Kinsta en tant que rédacteur technique. Il est un enseignant passionné par l'open source et a écrit plus de 200 articles techniques, principalement autour de JavaScript et de ses frameworks.