Developpez.com

Plus de 2 000 forums
et jusqu'à 5 000 nouveaux messages par jour

Les unions de types en TypeScript

Spécifications provisoires

Cet article présente le concept puissant des unions de types. Ce sont les spécifications en cours d'implémentation (au moment de la rédaction de l'article) et qui pourraient rendre TypeScript assez unique dans le paysage des langages avec typage.

Les commentaires et les suggestions d'amélioration sont les bienvenus, alors, après votre lecture, n'hésitez pas. Commentez.

Article lu   fois.

Les deux auteur et traducteur

Traducteur : Profil Pro

Liens sociaux

Viadeo Twitter Facebook Share on Google+   

I. Avant-propos

Anders Hejlsberg, le concepteur de TypeScript, a annoncé il y a deux semaines que son équipe travaillait à l'implémentation des unions de types.

L'union de types est un concept assez simple à comprendre, mais étrangement peu répandu dans les langages typés. Voici la traduction de la spécification #805 rédigée par Ryan Cavanaugh dans sa version originale.

II. L'opérateur | sur les types

Ceci est un résumé des spécifications imaginées par Anders Hejlsberg.

II-A. Cas d'utilisation

Beaucoup de bibliothèques JavaScript acceptent des valeurs de plus d'un seul type. Par exemple, la propriété AJAX jsonp avec jQuery peut être soit false (i.e. de type boolean) ou une chaîne de caractères (type string). Les fichiers de définition TypeScript (.d.ts) doivent représenter cette propriété avec le type any, perdant ainsi la sécurité du typage.

De même, la configuration du service HTTP d'AngularJS (https://docs.angularjs.org/api/ng/service/$http#usage) possède des propriétés de type boolean, Cache, number ou encore Promise.

II-B. Solutions de contournement actuelles

Cette lacune peut souvent être contournée avec des surcharges de fonction, mais il n'y a pas d'équivalent pour les propriétés des objets, les contraintes sur les types, ou d'autres rôles concernant les types.

III. Présentation

III-A. Syntaxe

Le nouvel opérateur |, lorsqu'il est utilisé pour séparer deux types, produit une union de types représentant une valeur qui est de l'un des types en entrée.

Exemple :

 
Sélectionnez
interface Settings {
    foo: number|string;
}
function setSetting(s: Settings) { /* ... */ }
setSettings({ foo: 42 }); // OK
setSettings({ foo: '100' }); // OK
setSettings({ foo: false }); // Error, false is not assignable to number|string

Plusieurs types peuvent être combinés de cette façon :

 
Sélectionnez
function process(n: string|HTMLElement|JQuery) { /* ... */ }
process('foo'); // OK
process($('div')); // OK
process(42); // Error

N'importe quel type est un opérande valide pour l'opérateur |. Voici quelques exemples et comment ils seraient analysés :

 
Sélectionnez
var x: number|string[]; // x is a number or a string[]
var y: number[]|string[]; // y is a number[] or a string[]
var z: Array<number|string>; // z is an array of number|string
var t: F|typeof G|H; // t is an F, typeof G, or H
var u: () => string|number; // u is a function that returns a string|number
var v: { (): string; }|number; // v is a function that returns a string, or a number

Notez que les parenthèses n'étant pas nécessaires pour lever l'ambiguïté, elles ne sont pas acceptées.

III-B. Interprétation

La signification de A|B est un type qui est soit un A, soit un B. En particulier, c'est différent d'un type qui combinerait tous les membres de A et de B. Nous examinerons cela dans des exemples plus loin.

IV. Sémantique

IV-A. Notions de base

Quelques règles simples :

  • identité : A|A est équivalent à ;
  • commutativité : A|B est équivalent à B|;
  • associativité : (A|B)|C est équivalent à A|(B|C);
  • effacement du sous-type : A|B est équivalent à A si B est un sous-type de A.

IV-B. Propriétés (attributs)

Le type A|B possède une propriété P de type X|Y si A possède une propriété P de type X et B possède une propriété P de type Y. Ces propriétés doivent soit être à la fois publiques, soit provenir du même site de déclaration (tel que spécifié dans les règles pour private/protected). Si l'une des propriétés est facultative, la propriété qui en résulte est également facultative.

Exemple :

 
Sélectionnez
interface Car {
    weight: number;
    gears: number;
    type: string;
}
interface Bicycle {
    weight: number;
    gears: boolean;
    size: string;
}
var transport: Car|Bicycle = /* ... */;
var w: number = transport.weight; // OK
var g = transport.gears; // OK, g is of type number|boolean
 
console.log(transport.type); // Error, transport does not have 'type' property
console.log((<Car>transport).type); // OK

V. Appel et construction de signatures

Le type A|B a une signature d'appel F si A a une signature d'appel F et B a une signature d'appel F.

Exemple :

 
Sélectionnez
var t: string|boolean = /* ... */;
console.log(t.toString()); // OK (both string and boolean have a toString method)

La même règle est appliquée pour construire signatures.

V-A. Signatures d'indice

Le type A|B a une signature d'indice [x: number]: T ou [x: string]: T si les deux A et B ont une signature d'indice de ce type.

VI. Réductibilité (assignability) et sous-typage

Nous décrivons ici la réductibilité ; le sous-typage est la même chose, sauf que « est réductible à » est remplacé par « est un sous-type de ».

Le type S est réductible au type T1|T2 si S est réductible à T1 ou si S est réductible à T2.

Exemple :

 
Sélectionnez
var x: string|number;
x = 'hello'; // OK, can assign a string to string|number
x = 42; // OK
x = { }; // Error, { } is not assignable to string or assignable to number

Le type S1|S2 est réductible au type T si les deux S1 et S2 sont réductibles à T.

Exemple :

 
Sélectionnez
var x: string|number = /* ... */;
var y: string = x; // Error, number is not assignable to string
var z: number = x; // Error, string is not assignable to number

En combinant les règles, le type S1|S2 est réductible au type T1|T2 si S1 est réductible à T1 ou T2 et S2 est réductible à T1 ou T2. Plus généralement, tous les types sur la partie droite de la réduction doivent être réduits à au moins un type sur la partie gauche.

Exemple :

 
Sélectionnez
var x: string|number;
var y: string|number|boolean;
x = y; // Error, boolean is not assignable to string or number
y = x; // OK (both string and number are assignable to string|number)

VII. Meilleur type commun

L'algorithme actuel du meilleur type commun (c.f. spécifications section 3.10) est seulement capable de produire un type déjà existant parmi les candidats, ou le type {}. Par exemple, le tableau [1, 2, "hello"] est de type {}[]. Avec la possibilité de représenter les unions de types, nous pouvons changer l'algorithme du meilleur type commun pour produire une union de types lorsqu'on est en présence d'un ensemble de candidats sans supertype.

Exemple :

 
Sélectionnez
class Animal { run(); }
class Dog extends Animal { woof(); }
class Cat extends Animal { meow(); }
class Rhino extends Animal { charge(); }
var x = [new Dog(), new Cat()];
// Current behavior: x is of type {}[]
// Proposed: x is of type Array<Dog|Cat>

Notez que dans ce cas, le type Dog|Cat est structurellement équivalent à Animal par rapport à ses membres, mais il serait une erreur d'essayer d'attribuer un Rhino à x[0], car Rhino n'est pas réductible à Cat ou Dog.

Le meilleur type commun est utilisé pour plusieurs inférences réalisées par le langage. Dans les cas

  • x || y,
  • z ? x : y,
  • z ? x : y et
  • [x, y],

le type résultant sera X | Y (où X est le type de x et Y est le type de y). Pour l'instruction return dans une fonction et pour l'inférence du type générique, nous allons exiger l'existence d'un supertype entre les candidats.

Exemple :

 
Sélectionnez
// Error, no best common type among 'string' and 'number'
function fn() {
    if(Math.random() > 0.5) {
        return 'hello';
    } else { 
        return 42;
    }
}
// OK with type annotation
function fn(): string|number {
    /* ... same as above ... */
}

VIII. Prochaines étapes possibles

VIII-A. Combinaison des membres de types

D'autres scénarios nécessitent un type construit à partir de A et B ayant tous ses membres présents dans l'un ou l'autre des deux types, mais pas dans les deux. Au lieu d'ajouter une nouvelle syntaxe de type, nous pouvons représenter cela facilement en supprimant la restriction qui fait que les clauses extends peuvent ne pas référencer les paramètres de type de leur déclaration.

Exemple :

 
Sélectionnez
interface HasFoo<T> extends T {
    foo: string;
}
interface Point {
    x: number;
    y: number;
}
var p: HasFoo<Point> = /* ... */;
console.log(p.foo); // OK
console.log(p.x.toString(); // OK

VIII-B. Signification locale des unions de types

Pour les unions de types où un opérande est une primitive, nous avons pu détecter certains schémas syntaxiques et ajuster le type d'un identifiant dans les blocs conditionnels.

Exemple :

 
Sélectionnez
var p: string|Point = /* ... */;
if(typeof p === 'string') {
    console.log(p); // OK, 'p' has type string in this block
} else {
    console.log(p.x.toString()); // OK, 'p' has type Point in this block
}

Cela pourrait également s'étendre à des vérifications d'appartenance :

 
Sélectionnez
interface Animal { run(); }
interface Dog extends Animal { woof(); }
interface Cat extends Animal { meow(); }
var x: Cat|Dog = /* ... */;
if(x.woof) {
   // x is 'Dog' here
}
if(typeof x.meow !== 'undefined') {
   // x is 'Cat' here
}

IX. Remerciements

Je remercie l'équipe TypeScript de Microsoft pour m'avoir autorisé à publier cette traduction, ainsi que Claude Leloup pour la relecture orthographique de cet article.

Les commentaires et les suggestions d'amélioration sont les bienvenus, alors, après votre lecture, n'hésitez pas. Commentez.

Vous avez aimé ce tutoriel ? Alors partagez-le en cliquant sur les boutons suivants : Viadeo Twitter Facebook Share on Google+   

  

Copyright © 2014 Equipe TypeScript Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.