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 !

TypeScript 4.7 RC s'accompagne de la prise en charge du module ECMAScript dans Node.js
Et propose un contrôle de la détection de module

Le , par Stéphane le calme

27PARTAGES

6  0 
Microsoft a annoncé la disponibilité de la RC (Release Candidate) de TypeScript 4.7 : « D'ici à la version stable de TypeScript 4.7, nous n'attendons aucun autre changement en dehors des corrections de bogues critiques ».

Quoi de neuf depuis la bêta ?

Après la version bêta, nous avons réalisé que typeof sur les champs #private avait des problèmes de compatibilité avec l'API. Nous nous sommes aussi demandé si typeof this.#somePrivate compose bien sur quelque chose de plus important : la déclaration émission. Par conséquent, la fonctionnalité ne sera pas dans TypeScript 4.7.

Depuis la version bêta, la syntaxe du mode de résolution est toujours disponible pour les directives /// <reference types="..." />; cependant, nous avons reçu des commentaires sur import type et voulions reconsidérer les besoins et la conception de la fonctionnalité. À son tour resolution-mode n'est disponible qu'à titre expérimental dans import type dans les versions Nightly de TypeScript.

Cette version inclut également une nouvelle commande d'éditeur d'aperçu pour Go to Source Definition. Cela peut être utile dans les cas où un ordinaire Go to Source Definition vous amènerait à un fichier de déclaration au lieu de la source JavaScript ou TypeScript réelle.

Certaines modifications majeures depuis la version bêta, notamment les règles relatives aux contraintes de paramètres de type plus strictes dans strictNullChecks, ont été annulées. Malheureusement, certains changements apparemment inoffensifs ont introduit des règles plus strictes autour des spreads JSX et des génériques utilisés dans les chaînes de modèle, qui sont de nouvelles pauses depuis la version bêta.


Prise en charge du module ECMAScript dans Node.js

Depuis quelques années, Node.js travaille pour supporter les modules ECMAScript (ESM). Cela a été une fonctionnalité très difficile à implémenter, car l'écosystème Node.js est construit sur un système de modules différent appelé CommonJS (CJS). L'interopérabilité entre les deux apporte de grands défis. Cependant, la prise en charge d'ESM dans Node.js a été largement implémentée dans Node.js 12 et versions ultérieures. Autour de TypeScript 4.5, Microsoft a déployé une prise en charge en nightly uniquement pour ESM dans Node.js afin d'obtenir des commentaires des utilisateurs et de permettre aux auteurs de bibliothèques de se préparer à une prise en charge plus large.

TypeScript 4.7 ajoute cette fonctionnalité avec deux nouveaux paramètres de module : node16 et nodenext.

Code TypeScript : Sélectionner tout
1
2
3
4
5
{ 
    "compilerOptions": { 
        "module": "nodenext", 
    } 
}

Ces nouveaux modes apportent quelques fonctionnalités de haut niveau que nous allons explorer ici.

type dans package.json et les nouvelles extensions

Node.js prend en charge un nouveau paramètre dans package.json appelé type. "type" peut être défini sur "module" ou "commonjs".

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
{ 
    "name": "my-package", 
    "type": "module", 
  
    "//": "...", 
    "dependencies": { 
    } 
}

Ce paramètre contrôle si les fichiers .js sont interprétés comme des modules ES ou des modules CommonJS, et par défaut sur CommonJS lorsqu'il n'est pas défini. Lorsqu'un fichier est considéré comme un module ES, quelques règles différentes entrent en jeu par rapport à CommonJS :
  • Les instructions d'importation/exportation (et l'attente de niveau supérieur dans nodenext) peuvent être utilisées.
  • Les chemins d'importation relatifs nécessitent des extensions complètes (nous devons écrire import "./foo.js" au lieu de import "./foo").
  • Les importations peuvent être résolues différemment des dépendances dans node_modules.
  • Certaines valeurs de type global telles que require() et process ne peuvent pas être utilisées directement.
  • Les modules CommonJS sont importés selon certaines règles spéciales.

Pour superposer le fonctionnement de TypeScript dans ce système, les fichiers .ts et .tsx fonctionnent désormais de la même manière. Lorsque TypeScript trouve un fichier .ts, .tsx, .js ou .jsx, il recherche un package.json pour voir si ce fichier est un module ES et l'utilise pour déterminer :
  • comment trouver d'autres modules que ce fichier importe
  • et comment transformer ce fichier s'il produit des sorties

Lorsqu'un fichier .ts est compilé en tant que module ES, les instructions d'import/export ECMAScript sont laissées seules dans la sortie .js ; lorsqu'il est compilé en tant que module CommonJS, il produira la même sortie que vous obtenez aujourd'hui sous --module commonjs.

Cela signifie également que les chemins se résolvent différemment entre les fichiers .ts qui sont des modules ES et ceux qui sont des modules CJS. Par exemple, supposons que vous ayez le code suivant aujourd'hui :

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
// ./foo.ts 
export function helper() { 
    // ... 
} 
  
// ./bar.ts 
import { helper } from "./foo"; // only works in CJS 
  
helper();

Ce code fonctionne dans les modules CommonJS, mais échouera dans les modules ES car les chemins d'importation relatifs doivent utiliser des extensions. En conséquence, il devra être réécrit pour utiliser l'extension de la sortie de foot.ts - donc bar.ts devra plutôt importer depuis ./foo.js.

Code TypeScript : Sélectionner tout
1
2
3
4
// ./bar.ts 
import { helper } from "./foo.js"; // works in ESM & CJS 
  
helper();

Cela peut sembler un peu lourd au début, mais les outils TypeScript tels que les importations automatiques et la saisie semi-automatique le feront généralement pour vous.

Une autre chose à mentionner est le fait que cela s'applique également aux fichiers .d.ts. Lorsque TypeScript trouve un fichier .d.ts dans le package, il est interprété en fonction du package contenant.

Nouvelles extensions de fichiers

Le champ type dans package.json est agréable car il nous permet de continuer à utiliser les extensions de fichier .ts et .js, ce qui peut être pratique ; cependant, vous devrez parfois écrire un fichier qui diffère du type spécifié. Vous pourriez aussi préférer être toujours explicite.

Node.js prend en charge deux extensions pour vous aider : .mjs et .cjs. Les fichiers .mjs sont toujours des modules ES et les fichiers .cjs sont toujours des modules CommonJS, et il n'y a aucun moyen de les remplacer.

À son tour, TypeScript prend en charge deux nouvelles extensions de fichier source : .mts et .cts. Lorsque TypeScript les émet vers des fichiers JavaScript, il les émet vers .mjs et .cjs respectivement.

De plus, TypeScript prend également en charge deux nouvelles extensions de fichier de déclaration : .d.mts et .d.cts. Lorsque TypeScript génère des fichiers de déclaration pour .mts et .cts, leurs extensions correspondantes seront .d.mts et .d.cts.

L'utilisation de ces extensions est entièrement facultative, mais sera souvent utile même si vous choisissez de ne pas les utiliser dans le cadre de votre flux de travail principal.

Interopérabilité CommonJS

Node.js permet aux modules ES d'importer des modules CommonJS comme s'il s'agissait de modules ES avec une exportation par défaut.

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
// ./foo.cts 
export function helper() { 
    console.log("hello world!"); 
} 
  
// ./bar.mts 
import foo from "./foo.cjs"; 
  
// prints "hello world!" 
foo.helper();

Dans certains cas, Node.js synthétise également des exportations nommées à partir de modules CommonJS, ce qui peut être plus pratique. Dans ces cas, les modules ES peuvent utiliser une importation "de style espace de noms" (c'est-à-dire import * as foo from "...")) ou des importations nommées (c'est-à-dire import { helper } from "...")

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
// ./foo.cts 
export function helper() { 
    console.log("hello world!"); 
} 
  
// ./bar.mts 
import { helper } from "./foo.cjs"; 
  
// prints "hello world!" 
helper();

Il n'y a pas toujours un moyen pour TypeScript de savoir si ces importations nommées seront synthétisées, mais TypeScript se trompera en étant permissif et utilisera certaines heuristiques lors de l'importation à partir d'un fichier qui est définitivement un module CommonJS.

Une note spécifique à TypeScript concernant l'interopérabilité est la syntaxe suivante :

Code TypeScript : Sélectionner tout
import foo = require("foo");

Dans un module CommonJS, cela se résume à un appel require() , et dans un module ES, cela importe createRequire pour obtenir la même chose. Cela rendra le code moins portable sur les runtimes comme le navigateur (qui ne prend pas en charge require()), mais sera souvent utile pour l'interopérabilité. À son tour, vous pouvez écrire l'exemple ci-dessus en utilisant cette syntaxe comme suit :

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
// ./foo.cts 
export function helper() { 
    console.log("hello world!"); 
} 
  
// ./bar.mts 
import foo = require("./foo.cjs"); 
  
foo.helper()

Enfin, il convient de noter que le seul moyen d'importer des fichiers ESM à partir d'un module CJS consiste à utiliser des appels dynamiques import(). Cela peut présenter des défis, mais c'est le comportement dans Node.js aujourd'hui.

package.json Exportations, importations et auto-référencement

Node.js prend en charge un nouveau champ pour définir les points d'entrée dans package.json appelé "exports". Ce champ est une alternative plus puissante à la définition de "main" dans package.json, et peut contrôler quelles parties de votre package sont exposées aux consommateurs.

Voici un package.json qui prend en charge des points d'entrée séparés pour CommonJS et ESM*:

Code TypeScript : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// package.json 
{ 
    "name": "my-package", 
    "type": "module", 
    "exports": { 
        ".": { 
            // Entry-point for `import "my-package"` in ESM 
            "import": "./esm/index.js", 
  
            // Entry-point for `require("my-package") in CJS 
            "require": "./commonjs/index.cjs", 
        }, 
    }, 
  
    // CJS fall-back for older versions of Node.js 
    "main": "./commonjs/index.cjs", 
}

Avec la prise en charge de Node d'origine de TypeScript, il rechercherait un champ "main", puis rechercherait les fichiers de déclaration correspondant à cette entrée. Par exemple, si "main" pointe vers ./lib/index.js, TypeScript recherchera un fichier appelé ./lib/index.d.ts. Un auteur de package pourrait remplacer cela en spécifiant un champ séparé appelé "types" (par exemple "types": "./types/index.d.ts").

Le nouveau support fonctionne de la même manière avec les conditions d'importation. Par défaut, TypeScript superpose les mêmes règles avec les conditions d'importation - si vous écrivez une importation à partir d'un module ES, il recherchera le champ d'importation et, à partir d'un module CommonJS, il examinera le champ requis. S'il les trouve, il cherchera un fichier de déclaration correspondant. Si vous devez pointer vers un emplacement différent pour vos déclarations de type, vous pouvez ajouter une condition d'importation "types".

Code TypeScript : 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
// package.json 
{ 
    "name": "my-package", 
    "type": "module", 
    "exports": { 
        ".": { 
            // Entry-point for `import "my-package"` in ESM 
            "import": { 
                // Where TypeScript will look. 
                "types": "./types/esm/index.d.ts", 
  
                // Where Node.js will look. 
                "default": "./esm/index.js" 
            }, 
            // Entry-point for `require("my-package") in CJS 
            "require": { 
                // Where TypeScript will look. 
                "types": "./types/commonjs/index.d.cts", 
  
                // Where Node.js will look. 
                "default": "./commonjs/index.cjs" 
            }, 
        } 
    }, 
  
    // Fall-back for older versions of TypeScript 
    "types": "./types/index.d.ts", 
  
    // CJS fall-back for older versions of Node.js 
    "main": "./commonjs/index.cjs" 
}

Notez que la condition "types" doit toujours venir en premier dans "exports".

TypeScript prend également en charge le champ "imports" de package.json de la même manière (recherche de fichiers de déclaration à côté des fichiers correspondants) et prend en charge les packages qui se référencent eux-mêmes. Ces fonctionnalités ne sont généralement pas aussi impliquées, mais sont prises en charge.

Contrôle de la détection de module

Un problème avec l'introduction des modules dans JavaScript était l'ambiguïté entre le code "script" existant et le nouveau code de module. Le code JavaScript dans un module s'exécute légèrement différemment et a des règles de portée différentes, de sorte que les outils doivent prendre des décisions sur la façon dont chaque fichier s'exécute. Par exemple, Node.js nécessite que les points d'entrée du module soient écrits dans un .mjs, ou qu'il ait un package.json à proximité avec "type": "module". TypeScript traite un fichier comme un module chaque fois qu'il trouve une instruction d'importation ou d'exportation dans un fichier, mais sinon, il supposera qu'un fichier .ts ou .js est un fichier de script agissant sur la portée globale.

Cela ne correspond pas tout à fait au comportement de Node.js où le package.json peut modifier le format d'un fichier, ou le paramètre --jsx react-jsx, où tout fichier JSX contient une importation implicite vers une usine JSX. Cela ne correspond pas non plus aux attentes modernes où la plupart des nouveaux codes TypeScript sont écrits en pensant aux modules.

C'est pourquoi TypeScript 4.7 introduit une nouvelle option appelée moduleDetection. moduleDetection peut prendre 3 valeurs : "auto" (la valeur par défaut), "legacy" (le même comportement que 4.6 et antérieur) et "force".

En mode "auto", TypeScript recherchera non seulement les instructions d'importation et d'exportation, mais il vérifiera également si :
  • le champ "type" dans package.json est défini sur "module" lors de l'exécution sous --module nodenext/--module node12, et
  • vérifiera si le fichier actuel est un fichier JSX lors de l'exécution sous --jsx react-jsx

Dans les cas où vous souhaitez que chaque fichier soit traité comme un module, le paramètre "forcer" garantit que chaque fichier sans déclaration est traité comme un module. Cela sera vrai quelle que soit la configuration de module, moduleResoluton et jsx.

Pendant ce temps, l'option "héritée" revient simplement à l'ancien comportement consistant à rechercher uniquement les instructions d'importation et d'exportation pour déterminer si un fichier est un module.

Source : Microsoft

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