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 type est un concept assez simple à comprendre mais étrangement peu répandu dans les langages typés. Voici la traduction de la spécification #805 :

L'opérateur | pour les types

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

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 soit boolean ou Cache ou number ou encore Promise.

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.

Introduction

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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
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 |. Quelques exemples et comment ils seraient analysés :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
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 ne sont pas nécessaires pour lever l'ambiguïté, de sorte qu'elles ne sont pas acceptées.

Interprétation

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

Sémantique

Notions de base

Quelques règles simples:
  • Identité : A|A est équivalent à A
  • Commutativité : A|B est équivalent à B|A
  • 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


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 être soit à la fois publiques, ou doivent 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é est facultative, la propriété qui en résulte est également facultative.

Exemple :
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
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
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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
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.

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.

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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
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)
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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
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 en termes de ses membres, mais il ce 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 les instructions return dans une fonction et l'inférence du type générique, nous allons exiger de l'existence d'un supertype entre les candidats.

Exemple :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
11
12
// 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 ... */
}
Prochaines étapes possibles

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 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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
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
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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
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 :
Code : Sélectionner tout - Visualiser dans une fenêtre à part
1
2
3
4
5
6
7
8
9
10
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
}