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 version 4.8 de TypeScript, elle améliore les performances de --build, --watch et --incremental,
Les types ne peuvent pas être importés/exportés dans les fichiers JavaScript

Le , par Bruno

78PARTAGES

6  0 
Dans un article publié sur son blog le 25 août, Microsoft a annoncé la version 4.8 de TypeScript, extension de JavaScript qui ajoute des types statiques et la vérification de type. Elle autorise le code dans les constructeurs avant super(), améliore les vérifications de la profondeur de récursion et analyse le flux de contrôle pour les paramètres dépendants.

TypeScript est un langage de programmation libre et open source développé par Microsoft qui a pour but d'améliorer et de sécuriser la production de code JavaScript. Il s'agit d'un surensemble du JavaScript qui apporte un typage statique et optionnel des variables. Il permet de détecter certaines erreurs en amont et se compile en JavaScript pour une utilisation côté navigateur ou côté serveur à l'aide de NodeJS. Notons qu’avec l’utilitaire npm les développeurs JavaScript peuvent partager et réutiliser facilement leur code. Il facilite la mise à jour du code et est distribué avec Node.js. TypeScript présente plusieurs avantages :

  • la prévention des bogues et la maintenabilité du code ;
  • la prise en charge des interfaces, des sous-interfaces, des classes, des sous-classes ;
  • la capacité de programmer en orienté objet avec l’héritage des membres privés et des interfaces.


Etant donné qu’il s’agit d’un langage qui s'appuie sur JavaScript et ajoute une syntaxe pour les types, ilpermet d'intégrer les attentes et les hypothèses dans le code, et ces hypothèses peuvent ensuite être vérifiées par le vérificateur de type TypeScript. Cette vérification peut aider à éviter les fautes de frappe, l'appel de valeurs non initialisées, le mélange d'arguments pour les fonctions, etc. Les types vont au-delà de la vérification et sont utilisés pour vous offrir une expérience d'édition puissante à la fois pour TypeScript et JavaScript, en permettant la complétion de code, les définitions de type, le renommage.

Voici, ci-dessous, les nouveautés apportées par TypeScript 4.8 :

Amélioration de la réduction des intersections, de la compatibilité des unions et de l'étroitesse du champ d'application

TypeScript 4.8 apporte une série d'améliorations de correction et de cohérence sous --strictNullChecks. Ces changements affectent la façon dont les types d'intersection et d'union fonctionnent, et sont exploités dans la façon dont TypeScript réduit les types. Par exemple, unknown est proche dans l'esprit du type union {} | null | undefined car il accepte null, undefined, et tout autre type. TypeScript reconnaît maintenant cela, et permet les affectations de unknown à {} | null | undefined.

Code : Sélectionner tout
1
2
3
4
function f(x: unknown, y: {} | null | undefined) {
    x = y; // always worked
    y = x; // used to error, now works
}

Un autre changement est que l'intersection de {} avec tout autre type d'objet est simplifiée jusqu'à ce type d'objet. Cela signifie que nous avons pu réécrire NonNullable en utilisant simplement une intersection avec {}, car {} & null et {} & undefined sont simplement rejetés.

Code : Sélectionner tout
1
2
- type NonNullable<T> = T extends null | undefined ? never : T;
+ type NonNullable<T> = T & {};

Il s'agit d'une amélioration car les types d'intersection comme celui-ci peuvent être réduits et assignés, alors que les types conditionnels ne le peuvent pas actuellement. Ainsi, NonNullable<NonNullable<T>> se simplifie maintenant au moins en NonNullable<T>, alors que ce n'était pas le cas auparavant.

Code : Sélectionner tout
1
2
3
4
function foo<T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) {
    x = y; // always worked
    y = x; // used to error, now works
}

Ces changements nous ont également permis d'apporter des améliorations sensibles à l'analyse du flux de contrôle et à la réduction des types. Par exemple, le type inconnu est maintenant réduit comme {} | null | undefined dans les branches véridiques.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function narrowUnknownishUnion(x: {} | null | undefined) {
    if (x) {
        x;  // {}
    }
    else {
        x;  // {} | null | undefined
    }
}

function narrowUnknown(x: unknown) {
    if (x) {
        x;  // used to be 'unknown', now '{}'
    }
    else {
        x;  // unknown
    }
}

Les valeurs génériques sont également limitées de la même manière. Lorsqu'il vérifie qu'une valeur n'est pas nulle ou indéfinie, TypeScript la croise désormais avec {} - ce qui revient à dire qu'elle est NonNullable. En combinant les nombreux changements, nous pouvons maintenant définir la fonction suivante sans aucune assertion de type.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
function throwIfNullable<T>(value: T): NonNullable<T> {
    if (value === undefined || value === null) {
        throw Error("Nullable value!");
    }

    // Used to fail because 'T' was not assignable to 'NonNullable<T>'.
    // Now narrows to 'T & {}' and succeeds because that's just 'NonNullable<T>'.
    return value;
}

se réduit désormais à T & {}, et est désormais identique à NonNullable<T> - le corps de la fonction fonctionne donc sans syntaxe spécifique à TypeScript. En soi, ces changements peuvent sembler mineurs - mais ils représentent des corrections pour de nombreuses coupures de papier qui ont été signalées pendant plusieurs années.

Inférence améliorée pour les types d'inférence dans les types de chaînes de caractères de modèle

TypeScript a récemment introduit un moyen d'ajouter des contraintes étendues pour inférer les variables de type dans les types conditionnels.

Code : Sélectionner tout
1
2
3
4
// Grabs the first element of a tuple if it's assignable to 'number',
// and returns 'never' if it can't find one.
type TryGetNumberIfFirst<T> =
    T extends [infer U extends number, ...unknown[]] ? U : never;

Si ces types déduits apparaissent dans un modèle de type chaîne et sont contraints à un type primitif, TypeScript essaiera désormais d'analyser un type littéral.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
// SomeNum used to be 'number'; now it's '100'.
type SomeNum = "100" extends `${infer U extends number}` ? U : never;

// SomeBigInt used to be 'bigint'; now it's '100n'.
type SomeBigInt = "100" extends `${infer U extends bigint}` ? U : never;

// SomeBool used to be 'boolean'; now it's 'true'.
type SomeBool = "true" extends `${infer U extends boolean}` ? U : never;
Cela peut maintenant mieux exprimer ce qu'une bibliothèque fera au moment de l'exécution, et donner des types plus précis. Une note à ce sujet est que lorsque TypeScript analyse ces types littéraux, il essaiera avidement d'analyser autant de ce qui ressemble au type primitif approprié ; cependant, il vérifie ensuite si l'impression de cette primitive correspond au contenu de la chaîne. En d'autres termes, TypeScript vérifie si le passage de la chaîne de caractères, à la primitive, et inversement, correspond. S'il ne voit pas que la chaîne de caractères peut être « arrondie », alors il reviendra au type primitif de base.

Code : Sélectionner tout
1
2
// JustNumber is `number` here because TypeScript parses out `"1.0"`, but `String(Number("1.0"))` is `"1"` and doesn't match.
type JustNumber = "1.0" extends `${infer T extends number}` ? T : never;

Améliorations des performances de --build, --watch, et --incremental

TypeScript 4.8 introduit plusieurs optimisations qui devraient accélérer les scénarios autour de --watch et --incremental, ainsi que les constructions de références de projet utilisant --build. Par exemple, TypeScript est maintenant capable d'éviter de passer du temps à mettre à jour les timestamps pendant les changements no-op en mode --watch, ce qui rend les reconstructions plus rapides et évite de perturber les autres outils de construction qui pourraient être à l'affût de la sortie de TypeScript. De nombreuses autres optimisations permettant de réutiliser les informations entre --build, --watch et --incremental ont également été introduites.

Quelle est l'importance de ces améliorations ? Eh bien, sur une base de code interne assez importante, nous avons constaté des réductions de temps de l'ordre de 10 à 25 % sur de nombreuses opérations communes simples, avec des réductions de temps d'environ 40 % dans des scénarios sans changement. Nous avons également obtenu des résultats similaires sur la base de code TypeScript.

Erreurs lors de la comparaison des littéraux d'objets et de tableaux

Dans de nombreux langages, des opérateurs comme == effectuent ce que l'on appelle une égalité de « valeur » sur les objets. Par exemple, en Python, il est possible de vérifier si une liste est vide en vérifiant si une valeur est égale à la liste vide en utilisant ==.

Code : Sélectionner tout
1
2
3
if people_at_home == []:
    print("here's where I lie, broken inside. </3")
    adopt_animals()

Ce n'est pas le cas en JavaScript, où == et === entre objets (et donc, tableaux) vérifient si les deux références pointent vers la même valeur. Nous pensons qu'un code similaire en JavaScript est, au mieux, un coup de pouce pour les développeurs JavaScript, et au pire, un bogue dans le code de production. C'est pourquoi TypeScript interdit désormais le code suivant.

Code : Sélectionner tout
1
2
3
4
5
6
if (peopleAtHome === []) {
//  ~~~~~~~~~~~~~~~~~~~
// This condition will always return 'false' since JavaScript compares objects by reference, not value.
    console.log("here's where I lie, broken inside. </3")
    adoptAnimals();
}

Inférence améliorée à partir des modèles de liaison

Dans certains cas, TypeScript récupère un type à partir d'un modèle de liaison pour effectuer de meilleures inférences.
Code : Sélectionner tout
declare function chooseRandomly<T>(x: T, y: T): T;
Code : Sélectionner tout
1
2
3
4
5
6
7
8
let [a, b, c] = chooseRandomly([42, true, "hi!"], [0, false, "bye!"]);
//   ^  ^  ^
//   |  |  |
//   |  |  string
//   |  |
//   |  boolean
//   |
//   number
Lorsque chooseRandomly doit trouver un type pour T, il regarde principalement [42, true, "hi !"] et [0, false, "bye !"] ; mais TypeScript doit déterminer si ces deux types doivent être Array<number | boolean | string> ou le type tuple [number, boolean, string]. Pour ce faire, il va chercher des candidats existants comme un indice pour voir s'il existe des types de tuple. Lorsque TypeScript voit le motif de liaison [a, b, c], il crée le type [any, any, any], et ce type est choisi comme candidat de faible priorité pour T, qui est également utilisé comme indice pour les types [42, true, "hi !"] et [0, false, "bye !"]

Cette méthode était bonne pour chooseRandomly, mais qu'elle était insuffisante dans d'autres cas. Par exemple, prenez le code suivant :

Code : Sélectionner tout
1
2
3
declare function f<T>(x?: T): T;

let [x, y, z] = f();

Le motif de let [x, y, z] indiquait que f devait produire un tuple [any, any, any] ; mais f ne devrait vraiment pas changer son argument de type en fonction d'un motif de let. Il ne peut pas soudainement faire apparaître une nouvelle valeur de type tableau en fonction de ce à quoi il est assigné, donc le type du motif de liaison a beaucoup trop d'influence sur le type produit. En outre, comme le type du motif de let est plein de anys, nous nous retrouvons avec x, y et z typés comme any.

Dans TypeScript 4.8, ces binding patterns ne sont jamais utilisés comme candidats pour les arguments de type. Au lieu de cela, ils sont simplement consultés au cas où un paramètre nécessite un type plus spécifique, comme dans notre exemple chooseRandomly. Si vous devez revenir à l'ancien comportement, il est toujours possible de fournir des arguments de type explicites.

Corrections de la surveillance des fichiers (en particulier à travers les checkouts git)

Nous avons eu un bug de longue date où TypeScript a un temps très difficile avec certains changements de fichiers en mode --watch et des scénarios d'éditeur. Parfois, les symptômes sont des erreurs erronées ou imprécises qui peuvent apparaître et qui nécessitent de redémarrer tsc ou VS Code. Ces erreurs se produisent fréquemment sur les systèmes Unix, et vous avez pu les voir après avoir enregistré un fichier avec vim ou échangé des branches dans git.

Cela a été causé par des hypothèses sur la façon dont Node.js gère les événements de renommage sur les systèmes de fichiers. Les systèmes de fichiers utilisés par Linux et macOS utilisent des inodes, et Node.js attachera des surveillants de fichiers aux inodes plutôt qu'aux chemins de fichiers. Ainsi, lorsque Node.js renvoie un objet de surveillance, il peut surveiller un chemin ou un inode en fonction de la plate-forme et du système de fichiers.

Pour être un peu plus efficace, TypeScript essaie de réutiliser les mêmes objets watcher s'il détecte qu'un chemin existe toujours sur le disque. C'est là que les choses se gâtent, car même si un fichier existe toujours à ce chemin, un fichier distinct peut avoir été créé, et ce fichier aura un inode différent. Ainsi, TypeScript finissait par réutiliser l'objet watcher au lieu d'installer un nouveau watcher à l'emplacement d'origine, et de surveiller les changements dans ce qui pourrait être un fichier totalement non pertinent.

Donc TypeScript 4.8 gère maintenant ces cas sur les systèmes d'inode et installe correctement un nouveau watcher et corrige ce problème.

Amélioration des performances de Find-All-References

Lors de l'exécution de find-all-references dans votre éditeur, TypeScript est maintenant capable d'agir un peu plus intelligemment en agrégeant les références. Cela a réduit d'environ 20 % le temps que TypeScript a pris pour rechercher un identifiant largement utilisé dans sa propre base de code.

Exclusion de fichiers spécifiques des auto-importations

TypeScript 4.8 introduit une préférence d'éditeur pour exclure les fichiers des auto-importations. Dans Visual Studio Code, les noms de fichiers ou les expressions globales peuvent être ajoutés sous « Auto Import File Exclude Patterns » dans l'interface utilisateur des paramètres, ou dans un fichier .vscode/settings.json :

Code : Sélectionner tout
1
2
3
4
{
    // Note that `javascript.preferences.autoImportFileExcludePatterns` can be specified for JavaScript too.
    "typescript.preferences.autoImportFileExcludePatterns": [
      "**/node_modules/@types/node"
    ]
}
Cela peut être utile dans les cas où vous ne pouvez pas éviter d'avoir certains modules ou bibliothèques dans votre compilation, mais vous voulez rarement importer à partir d'eux. Ces modules peuvent avoir beaucoup d'exportations qui peuvent polluer la liste d'auto-importations et la rendre plus difficile à naviguer, et cette option peut aider dans ces situations.

Corrections et modifications de rupture

En raison de la nature des changements de système de type, il y a très peu de changements qui peuvent être faits qui n'affectent pas un certain code ; cependant, il y a quelques changements qui sont plus susceptibles de nécessiter l'adaptation du code existant.
Mises à jour de lib.d.ts.

Les génériques non contraints ne sont plus assignables à {}

Dans TypeScript 4.8, pour les projets avec strictNullChecks activé, TypeScript émet désormais correctement une erreur lorsqu'un paramètre de type non contraint est utilisé dans une position où null ou undefined ne sont pas des valeurs légales. Cela inclut tout type qui s'attend à {}, objet, ou un type d'objet avec toutes les propriétés optionnelles.

Un exemple simple peut être vu dans ce qui suit.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
// Accepts any non-null non-undefined value
function bar(value: {}) {
  Object.keys(value); // This call throws on null/undefined at runtime.
}

// Unconstrained type parameter T...
function foo<T>(x: T) {
    bar(x); // Used to be allowed, now is an error in 4.8.
    //  ~
    // error: Argument of type 'T' is not assignable to parameter of type '{}'.
}

foo(undefined);

Comme démontré ci-dessus, un code comme celui-ci présente un bogue potentiel - les valeurs null et undefined peuvent être indirectement transmises par ces paramètres de type non contraints à un code qui n'est pas censé observer ces valeurs.

Ce comportement sera également visible dans les positions de type. En voici un exemple :

Code : Sélectionner tout
1
2
3
interface Foo<T> {
  x: Bar<T>;
}
Code : Sélectionner tout
interface Bar<T extends {}> { }
Le code existant qui ne voulait pas gérer les éléments null et undefined peut être corrigé en propageant les contraintes appropriées.
Code : Sélectionner tout
1
2
- function foo<T>(x: T) {
+ function foo<T extends {}>(x: T) {
Une autre solution consisterait à vérifier l'existence de null et d'undefined au moment de l'exécution.

Code : Sélectionner tout
1
2
3
4
5
function foo<T>(x: T) {
+     if (x !== null && x !== undefined) {
          bar(x);
+     }
  }
Et si vous savez que, pour une raison quelconque, votre valeur générique ne peut pas être nulle ou indéfinie, vous pouvez simplement utiliser une assertion non nulle.

Code : Sélectionner tout
1
2
3
4
  function foo<T>(x: T) {
-     bar(x);
+     bar(x!);
  }

Les décorateurs sont placés sur les modificateurs des arbres syntaxiques de TypeScript

L'orientation actuelle des décorateurs dans TC39 signifie que TypeScript devra gérer une rupture en termes de placement des décorateurs. Auparavant, TypeScript supposait que les décorateurs seraient toujours placés avant tous les mots-clés/modificateurs. Par exemple

Code : Sélectionner tout
1
2
3
4
@decorator
export class Foo {
  // ...
}
Les décorateurs tels qu'ils sont proposés actuellement ne supportent pas cette syntaxe. Au lieu de cela, le mot-clé export doit précéder le décorateur.

Code : Sélectionner tout
1
2
3
export @decorator class Foo {
  // ...
}
Malheureusement, les arbres de TypeScript sont concrets plutôt qu'abstraits, et notre architecture s'attend à ce que les champs des nœuds de l'arbre syntaxique soient entièrement ordonnés avant ou après les autres. Pour supporter à la fois les décorateurs existants et les décorateurs tels que proposés, TypeScript devra analyser et intercaler les modificateurs et les décorateurs.

Pour ce faire, il expose un nouveau type alias appelé ModifierLike qui est un Modificateur ou un Décorateur.
Code : Sélectionner tout
export type ModifierLike = Modifier | Decorator;
Les décorateurs sont maintenant placés dans le même champ que les modificateurs qui est maintenant un NodeArray<ModifierLike> lorsqu'il est défini, et le champ entier est déprécié.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
- readonly modifiers?: NodeArray<Modifier> | undefined;
+ /**
+  * @deprecated ...
+  * Use `ts.canHaveModifiers()` to test whether a `Node` can have modifiers.
+  * Use `ts.getModifiers()` to get the modifiers of a `Node`.
+  * ...
+  */
+ readonly modifiers?: NodeArray<ModifierLike> | undefined;
Toutes les propriétés des décorateurs existants ont été marquées comme étant dépréciées et seront toujours indéfinies si elles sont lues. Le type a également été changé en indéfini afin que les outils existants sachent les gérer correctement.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
- readonly decorators?: NodeArray<Decorator> | undefined;
+ /**
+  * @deprecated ...
+  * Use `ts.canHaveDecorators()` to test whether a `Node` can have decorators.
+  * Use `ts.getDecorators()` to get the decorators of a `Node`.
+  * ...
+  */
+ readonly decorators?: undefined;
Pour éviter de nouveaux avertissements de dépréciation et d'autres problèmes, TypeScript expose désormais quatre nouvelles fonctions à utiliser à la place des propriétés decorators et modifiers. Il existe des prédicats individuels pour tester si un nœud possède des modificateurs et des décorateurs de support, ainsi que des fonctions d'accès respectives pour les saisir.

Code : Sélectionner tout
1
2
3
4
5
function canHaveModifiers(node: Node): node is HasModifiers;
function getModifiers(node: HasModifiers): readonly Modifier[] | undefined;

function canHaveDecorators(node: Node): node is HasDecorators;
function getDecorators(node: HasDecorators): readonly Decorator[] | undefined;
Comme exemple de la façon d'accéder aux modificateurs à partir d'un nœud, il est possible d'écrire

Code : Sélectionner tout
const modifiers = canHaveModifiers(myNode) ? getModifiers(myNode) : undefined;
Il est à noter que chaque appel à getModifiers et getDecorators peut allouer un nouveau tableau.

Les types ne peuvent pas être importés/exportés dans les fichiers JavaScript

TypeScript autorisait auparavant les fichiers JavaScript à importer et exporter des entités déclarées avec un type, mais sans valeur, dans les déclarations d'importation et d'exportation. Ce comportement était incorrect, car les importations et exportations nommées pour des valeurs qui n'existent pas provoquent une erreur d'exécution sous les modules ECMAScript. Lorsqu'un fichier JavaScript est soumis à un contrôle de type sous --checkJs ou par le biais d'un commentaire // @ts-check, TypeScript émet désormais une erreur.

Code : Sélectionner tout
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// @ts-check

// Will fail at runtime because 'SomeType' is not a value.
import { someValue, SomeType } from "some-module";

/**
 * @type {SomeType}
 */
export const myValue = someValue;

/**
 * @typedef {string | number} MyType
 */

// Will fail at runtime because 'MyType' is not a value.
export { MyType as MyExportedType };
Pour faire référence à un type provenant d'un autre module, il est possible plutôt qualifier directement l'importation.

Les patrons de liaison ne contribuent pas directement aux candidats à l'inférence

Comme mentionné ci-dessus, les binding patterns ne modifient plus le type de résultats d'inférence dans les appels de fonction. Les renommages inutilisés dans les binding patterns sont désormais des erreurs dans les signatures de type. La syntaxe d'annotation de type de TypeScript semble souvent pouvoir être utilisée pour déstructurer des valeurs. Par exemple, prenez la fonction suivante.

Code : Sélectionner tout
declare function makePerson({ name: string, age: number }): Person;
Cette signature peut faire penser que makePerson prend évidemment un objet avec une propriété name de type string et une propriété age de type number ; cependant, la syntaxe de déstructuration de JavaScript prend ici le dessus. makePerson dit bien qu'il va prendre un objet avec une propriété name et age, mais au lieu de spécifier un type pour eux, il dit juste qu'il renomme name et age en string et number respectivement.

Dans une construction de type pure, l'écriture de ce type de code est inutile et constitue généralement une erreur car les développeurs supposent généralement qu'ils écrivent une annotation de type. TypeScript 4.8 fait de ces annotations une erreur, sauf si elles sont référencées plus tard dans la signature. La façon correcte d'écrire la signature ci-dessus serait la suivante :

Code : Sélectionner tout
1
2
3
4
5
declare function makePerson(options: { name: string, age: number }): Person;

// ou

declare function makePerson({ name, age }: { name: string, age: number }): Person;

Source : Microsoft

Et vous ?

Quel est votre avis sur le sujet ?

Voir aussi :

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

Prisma : un ORM de nouvelle génération pour Node.js et TypeScript, pour concurrencer TypeORM et Sequelize et devenir la norme de l'industrie

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