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:
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 10 11 12 13 | // ./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 :
Code : | Sélectionner tout |
1 2 3 4 5 6 7 8 9 | src +-- client | +-- index.ts | +-- tsconfig.json +-- server | +-- index.ts | +-- tsconfig.json +-- shared +-- index.ts |
Code : | Sélectionner tout |
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 |
Code : | Sélectionner tout |
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.
Code : | Sélectionner tout |
1 2 3 | function call(fn, ...args) { return fn(...args); } |
Code : | Sélectionner tout |
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); } |
Code : | Sélectionner tout |
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 :
Code : | Sélectionner tout |
1 2 3 | function call<TS extends any[], R>(fn: (...args: TS) => R, ...args: TS): R { return fn(...args); } |
Code : | Sélectionner tout |
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"); |
Code : | Sélectionner tout |
function call(fn: (...args: [number, string]) => string, ...args: [number, string]): string
Code : | Sélectionner tout |
function call(fn: (arg1: number, arg2: string) => string, arg1: number, arg2: string): string
Code : | Sélectionner tout |
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`! |
Code : | Sélectionner tout |
1 2 3 | call((x, y) => { /* .... */ }, "hello", 100); // ^ ^ // `x` and `y` have their types inferred as `string` and `number` respectively. |
Code : | Sélectionner tout |
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 :
Code : | Sélectionner tout |
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); |
Code : | Sélectionner tout |
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"); |
Code : | Sélectionner tout |
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 :
Code : | Sélectionner tout |
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.
Code : | Sélectionner tout |
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:
Code : | Sélectionner tout |
type Foo = [...number[]]; // Equivalent to `number[]`.
Code : | Sélectionner tout |
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.
Code : | Sélectionner tout |
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 :
Code : | Sélectionner tout |
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(); } |
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 | 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 ?