La Release Candidate de TypeScript 3.0 vient de sortir
avec une gestion de projets améliorée
L'équipe TypeScript de Microsoft a publié le 12 juillet une version majeure du désormais fameux surensemble typé de JavaScript en Release Candidate. Pour l'installer, rien de plus simple :
npm install -g typescript@rc
Cette version est également disponible sous la forme de plugin pour Visual Studio 2017.
Référencement de projets TypeScript externes
Il est assez courant d'avoir plusieurs étapes de génération (build) différentes pour une bibliothèque ou une application. Un projet peut avoir un répertoire src et un répertoire test. On peut avoir son code front-end dans un dossier appelé client, son code back-end Node.js dans un dossier appelé server, chacun important du code à partir d'un dossier shared. Une base de code peut aussi être structurée sous la forme d'un « monorepo » avec beaucoup de projets qui dépendent les uns des autres de manière non triviale.
L'une des principales fonctionnalités de TypeScript 3.0 est appelée « références de projet », et vise à faciliter le travail avec ces scénarios.
Les références de projet permettent aux projets TypeScript de dépendre d'autres projets TypeScript, notamment en permettant aux fichiers tsconfig.json de référencer d'autres fichiers tsconfig.json. La spécification de ces dépendances facilite la division de votre code en projets plus petits, car il donne à TypeScript (et aux outils qui l'entourent) un moyen de comprendre l'ordre de génération et la structure de sortie. Cela permet des choses comme des générations plus rapides qui fonctionnent de manière incrémentale, ou encore une navigation transparente, la modification et le refactoring entre projets. Puisque la version 3.0 pose les fondations et expose les API, n'importe quel outil de génération (les IDE notamment) devrait pouvoir fournir cette fonctionnalité.
A quoi cela ressemble-t-il ?
Pour illustrer rapidement cette nouvelle possibilité, voici à quoi ressemble un tsconfig.json avec des références de projet:
Il y a deux nouveaux champs à noter ici : composite et references.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 // ./src/bar/tsconfig.json { "compilerOptions": { // Needed for project references. "composite": true, "declaration": true, // Other options... "outDir": "../../lib/bar", "strict": true, "module": "esnext", "moduleResolution": "node", }, "references": [ { "path": "../foo" } ] }
Le champ references spécifie simplement les autres fichiers tsconfig.json (ou les dossiers qui les contiennent directement). Chaque référence n'est actuellement qu'un objet ne contenant qu'un champ path, et permet à TypeScript de savoir que la génération du projet actuel nécessite une génération préalable des autres projets référencés.
Peut-être tout aussi important est le champ composite. Ce champ garantit que certaines options sont activées afin que ce projet puisse être référencé et généré de façon incrémentale pour tout projet qui en dépend. Être capable de reconstruire intelligemment et de façon incrémentale est important, puisque la vitesse de génération est l'une des principales raisons pour lesquelles nous pouvons avoir envie de décomposer un projet. Par exemple, si le projet front-end dépend de shared, et shared dépend de core, les API sur les références de projet peuvent non seulement détecter un changement dans core, mais aussi régénérer shared seulement si les types (i.e. les fichiers .d.ts) produit par core ont changé. Cela signifie qu'un changement de core ne nous force pas complètement à régénérer l'ensemble du projet. Pour cette raison, définir le paramètre composite oblige le champ declaration à être défini en même temps.
Mode --build
TypeScript 3.0 fournit un ensemble d'API pour les références de projet afin que d'autres outils puissent fournir rapidement cette génération incrémentale. A titre d'exemple, gulp-typescript le supporte déjà. Les références de projet devraient donc pouvoir être supportées par votre orchestrateur de build dans le futur.
Cependant, pour de nombreuses applications et bibliothèques simples, il est préférable de ne pas avoir besoin d'outils externes. C'est pourquoi tsc est désormais livré avec une nouvelle option --build.
tsc --build (raccourci, tsc -b) prend un ensemble de projets et les génère avec leurs dépendances. Lors de l'utilisation de ce nouveau mode de génération, l'option --build doit être définie en premier et peut être associée à certaines autres options :
--verbose : affiche toutes les étapes nécessaires à la génération
--dry : effectue une génération sans émettre de fichiers (ceci est utile avec --verbose)
--clean : tente de supprimer les fichiers de sortie en fonction des entrées
--force : force une regénération complète non incrémentale pour un projet
Organisation de la structure en sortie
Un avantage subtil, mais incroyablement utile des références de projet, est la possibilité de mapper de façon logique votre code source en entrée à ses sorties.
Si vous avez déjà essayé de partager du code TypeScript entre les parties client et serveur de votre application, vous avez peut-être rencontré des problèmes d'organisation de la structure en sortie.
Par exemple, si client/index.ts et server/index.ts font tous deux référence à shared/index.ts pour les projets suivants :
... alors, en essayant de générer client et server, on aboutit à cela :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9 src +-- client | +-- index.ts | +-- tsconfig.json +-- server | +-- index.ts | +-- tsconfig.json +-- shared +-- index.ts
au lieu de cela :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 lib +-- client | +-- client | | +-- index.js | +-- shared | +-- index.js +-- server +-- server | +-- index.js +-- shared +-- index.js
Notez que nous nous sommes retrouvés avec une copie de shared à la fois dans client et dans server. Aussi, nous avons passé un temps inutile à générer shared et avons introduit un niveau d'imbrication indésirable dans lib/client/client et lib/server/server.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 lib +-- client | +-- index.js +-- shared | +-- index.js +-- server +-- index.js
Le problème est que TypeScript lit les les fichiers .ts de manière « gourmande » (greedy) et essaie de les inclure dans une compilation donnée. Dans l'idéal, TypeScript devrait comprendre que ces fichiers n'ont pas besoin d'être générés dans la même compilation, et devrait plutôt passer directement aux fichiers .d.ts pour récupérer les informations de type.
La création d'un fichier tsconfig.json pour partager et utiliser des références de projet fait exactement cela. Il signale à TypeScript que
1. shared devrait être généré indépendamment, et que
2. lors de l'importation depuis ../shared, nous devrions rechercher les fichiers .d.ts dans son répertoire de sortie.
Cela évite de déclencher une double génération, et évite également de consommer accidentellement tout le contenu de shared.
Travaux à venir
Pour mieux comprendre les références de projets et savoir comment les utiliser, vous pouvez consulter le suivi des problèmes. Dans un avenir proche, une documentation plus détaillée sera publiée sur les références de projets et le mode --build.
Extraction et expansion de listes de paramètres avec des n-uplets
JavaScript peut nous laisser à penser que les listes de paramètres sont des valeurs de première classe -- soit en utilisant arguments ou ...args.
Notez ici que call fonctionne sur les fonctions ayant un nombre de paramètres quelconques. Contrairement à d'autres langages, nous n'avons pas besoin de définir de call1, call2, call3 comme suit:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 function call(fn, ...args) { return fn(...args); }
Cependant, jusqu'à présent, il n'y avait pas de manière très élégante pour typer statiquement en TypeScript sans avoir à déclarer un nombre fini de surcharges (overloads) :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11 function call1(fn, param1) { return fn(param1); } function call2(fn, param1, param2) { return fn(param1, param2); } function call3(fn, param1, param2, param3) { return fn(param1, param2, param3); }
Avouons que ce n'est pas terrible, car cela implique autant de surcharges que de besoins différents en termes de nombre de paramètres.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 // TODO (billg): 4 overloads should *probably* be enough for anybody? function call<T1, T2, T3, T4, R>(fn: (param1: T1, param2: T2, param3: T3, param4: T4) => R, param1: T1, param2: T2, param3: T3, param4: T4); function call<T1, T2, T3, R>(fn: (param1: T1, param2: T2, param3: T3) => R, param1: T1, param2: T2, param3: T3); function call<T1, T2, R>(fn: (param1: T1, param2: T2) => R, param1: T1, param2: T2); function call<T1, R>(fn: (param1: T1) => R, param1: T1): R; function call(fn: (...args: any[]) => any, ...args: any[]) { fn(...args); }
TypeScript 3.0 permet de mieux prendre en compte de tels scénarios en maintenant la généricité du paramètre résiduel ...args, et en déduisant de ce paramètre générique le n-uplet correspondant. Au lieu de déclarer chacune de ces surcharges, nous pouvons dire que le paramètre résiduel ...args de fn dérive d'un tableau, et que nous pouvons réutiliser cela pour le paramètre ...args passé dans call :
Quand la fonction call sera appelée, TypeScript essayera d'extraire la liste de paramètres de tout ce qui sera passé à fn, et transformera cela en un n-uplet:
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 function call<TS extends any[], R>(fn: (...args: TS) => R, ...args: TS): R { return fn(...args); }
Dans un premier temps, TypeScript infère le type générique TS en tant que [number, string] qui est ensuite utilisé sur le paramètre résiduel de call. La définition de la fonction call est transformée comme ce qui suit :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 function foo(x: number, y: string): string { return (x + y).toLowerCase(); } // `TS` is inferred as `[number, string]` call(foo, 100, "hello");
Puis, avec TypeScript 3.0, lorsqu'un n-uplet apparaît dans le type d'un paramètre résiduel, il est aplati (flattened) et expansé (spread) en autant de paramètres. Ce qui précède est considéré au final comme une suite de paramètres simples sans n-uplets :
Code : Sélectionner tout - Visualiser dans une fenêtre à part function call(fn: (...args: [number, string]) => string, ...args: [number, string]): string
Donc, le compilateur TypeScript 3.0 est capable de détecter les erreurs de type lorsque de mauvais paramètres sont passés :
Code : Sélectionner tout - Visualiser dans une fenêtre à part function call(fn: (arg1: number, arg2: string) => string, arg1: number, arg2: string): string
Le compilateur est aussi capable d'inférer les types à partir des autres paramètres :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7 function call<TS extends any[], R>(fn: (...args: TS) => R, ...args: TS): R { return fn(...args); } call((x: number, y: string) => y, "hello", "world"); // ~~~~~~~ // Error! `string` isn't assignable to `number`!
Encore mieux, il est possible de connaître depuis l'extérieur les types du n-uplet :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3 call((x, y) => { /* .... */ }, "hello", 100); // ^ ^ // `x` and `y` have their types inferred as `string` and `number` respectively.
Cependant, en coulisse, pour que cette fonctionnalité soit disponible, il a fallu améliorer le typage des n-uplets dans TypeScript.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5 function tuple<TS extends any[]>(...xs: TS): TS { return xs; } let x = tuple(1, 2, "hello"); // has type `[number, number, string]
Enrichissement du typage des n-uplets
Les listes de paramètres ne sont pas simplement des listes ordonnées de types dans la mesure où par exemple les derniers paramètres peuvent être facultatifs :
Le dernier paramètre peut aussi être un paramètre résiduel.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 // Both `y` and `z` are optional here. function foo(x: boolean, y = 100, z?: string) { // ... } foo(true); foo(true, undefined, "hello"); foo(true, 200);
Enfin, il existe une propriété à ne pas négliger concernant les listes de paramètres, à savoir qu'elles peuvent être vides :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8 // `rest` accepts any number of strings - even none! function foo(...rest: string[]) { // ... } foo(); foo("hello"); foo("hello", "world");
Donc, pour que les n-uplets puissent correspondre aux listes de paramètres, chacun de ces scénarios a dû être modélisé.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6 // Accepts no parameters. function foo() { // ... } foo();
Premièrement, les n-uplets autorisent maintenant les éléments facultatifs de fin :
Le type Coordinate crée un n-uplet avec une propriété optionnelle nommée 2 -- l'élément à l'index 2 peut ne pas être défini. Fait intéressant, puisque les n-uplets utilisent des types littéraux numériques pour leurs propriétés de longueur, la propriété longueur de Coordinate a le type 2 | 3.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4 /** * 2D, or potentially 3D, coordinate. */ type Coordinate = [number, number, number?];
Deuxièmement, les n-uplets autorisent maintenant les éléments résiduels à la fin.
Les éléments résiduels introduisent un comportement ouvert intéressant aux n-uplets. Le type OneNumberAndSomeStrings ci-dessus requiert que sa première propriété soit un nombre et autorise 0 ou plusieurs strings. Indexer avec un nombre arbitraire retournera un string | number puisque l'indice n'est pas connu à l'avance. De même, puisque la longueur du n-uplet n'est pas connue à l'avance, la propriété length est simplement number.
Code : Sélectionner tout - Visualiser dans une fenêtre à part type OneNumberAndSomeStrings = [number, ...string[]];
Fait à noter, quand aucun autre élément n'est présent, un élément résiduel dans un n-uplet est identique à lui-même:
Enfin, les n-uplets peuvent maintenant être vides ! Bien qu'il ne soit pas très utile en dehors des listes de paramètres, le type n-uplet vide peut être défini par []:
Code : Sélectionner tout - Visualiser dans une fenêtre à part type Foo = [...number[]]; // Equivalent to `number[]`.
Comme on peut s'y attendre, le n-uplet vide a une longueur de 0 et l'indexation avec un nombre renvoie le type never.
Code : Sélectionner tout - Visualiser dans une fenêtre à part type EmptyTuple = [];
Le type unknown
Le type any est le type le plus permissif dans TypeScript. Dans la mesure où il englobe tous les types possibles, aucune vérification n'est réalisée avant l'utilisation des propriétés d'une valeur de type any. De plus, une variable de type any peut recevoir des valeurs de tout autre type lors d'une affectation.
Son utilité n'est pas à démontrer, mais cela reste tout de même un peu laxiste dans pas mal de situations.
Il arrive des situations où nous voulons un type plus restrictif. Notamment dans le cas d'API où il peut être utile de signaler "cela peut être n'importe quelle valeur, donc vous devez effectuer un certain type de vérification avant de l'utiliser". On peut vouloir forcer les utilisateurs à faire de l'introspection sur les valeurs renvoyées dans une démarche de vérification.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 let foo: any = 10; // All of these will throw errors, but TypeScript // won't complain since `foo` has the type `any`. foo.x.prop; foo.y.prop; foo.z.prop; foo(); new foo(); upperCase(foo); foo `hello world!`; function upperCase(x: string) { return x.toUpperCase(); }
TypeScript 3.0 introduit un nouveau type appelé unknown qui fait exactement cela. Tout comme any, n'importe quelle valeur est assignable à unknown ; cependant, contrairement à any, nous ne pouvons pas accéder aux propriétés sur les valeurs avec le type unknown, ni ne pouvons les appeler / construire. De plus, les valeurs de type unknown ne peuvent être affectées uniquement qu'à des valeurs de type unknown ou any.
Par exemple, si on change dans le précédent exemple l'utilisation de any par unknown, cela provoque une erreur à chaque utilisation de foo :
Pour pouvoir manipuler après affection une variable de type unknown, une inférence de type (via un test) ou une assertion de type (as) vers un autre type que unknown est nécessaire.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 let foo: unknown = 10; // Since `foo` has type `unknown`, TypeScript // errors on each of these usages. foo.x.prop; foo.y.prop; foo.z.prop; foo(); new foo(); upperCase(foo); foo `hello world!`; function upperCase(x: string) { return x.toUpperCase(); }
Il s'agit ici des principales nouveautés, mais il y en a d'autres concernant par exemple une meilleure prise en charge de ReactJS.
Code : Sélectionner tout - Visualiser dans une fenêtre à part
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 let foo: unknown = 10; function hasXYZ(obj: any): obj is { x: any, y: any, z: any } { return !!obj && typeof obj === "object" && "x" in obj && "y" in obj && "z" in obj; } // Using a user-defined type guard... if (hasXYZ(foo)) { // ...we're allowed to access certain properties again. foo.x.prop; foo.y.prop; foo.z.prop; } // We can also just convince TypeScript we know what we're doing // by using a type assertion. upperCase(foo as string); function upperCase(x: string) { return x.toUpperCase(); }
source : Blog officiel de Microsoft
Que pensez-vous de cette version majeure ?
Les références de projet est-celle une fonctionnalité que vous pensez mettre en oeuvre prochainement dans vos projets ?
Partager