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 !

La Release Candidate de TypeScript 3.0 vient de sortir :
Tour d'horizon des nouveautés de cette version majeure du surensemble typé de JavaScript

Le , par yahiko

169PARTAGES

13  0 

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" }
    ]
}
Il y a deux nouveaux champs à noter ici : composite et references.

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
... alors, en essayant de générer client et server, on aboutit à cela :
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
au lieu de cela :
Code : Sélectionner tout
1
2
3
4
5
6
7
lib
+-- client
|   +-- index.js
+-- shared
|   +-- index.js
+-- server
    +-- 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.

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);
}
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
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);
}
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
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);
}
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.

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);
}
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
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");
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
function call(fn: (...args: [number, string]) => string, ...args: [number, string]): string
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
function call(fn: (arg1: number, arg2: string) => string, arg1: number, arg2: 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
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`!
Le compilateur est aussi capable d'inférer les types à partir des autres paramètres :

Code : Sélectionner tout
1
2
3
call((x, y) => { /* .... */ }, "hello", 100);
//    ^  ^
// `x` and `y` have their types inferred as `string` and `number` respectively.
Encore mieux, il est possible de connaître depuis l'extérieur les types du n-uplet :

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]
Cependant, en coulisse, pour que cette fonctionnalité soit disponible, il a fallu améliorer le typage des n-uplets dans TypeScript.

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);
Le dernier paramètre peut aussi être un paramètre résiduel.

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");
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
1
2
3
4
5
6
// Accepts no parameters.
function foo() {
    // ...
}

foo();
Donc, pour que les n-uplets puissent correspondre aux listes de paramètres, chacun de ces scénarios a dû être modélisé.

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?];
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.

Deuxièmement, les n-uplets autorisent maintenant les éléments résiduels à la fin.

Code : Sélectionner tout
type OneNumberAndSomeStrings = [number, ...string[]];
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.

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[]`.
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
type EmptyTuple = [];
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.

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();
}
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.

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();
}
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
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();
}
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.

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 ?

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