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 :
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 :
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 :
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 à 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.
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 :
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 :
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 :
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 :
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 :
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 :
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 :
// 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 :
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 :
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 :
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.