Spec Preview: Union types · Issue #805 · microsoft/TypeScript (original) (raw)
Updated 10/3 (see comment for changelog)
|
operator for types
This is a "spec preview" for a feature we're referring to as "union types". @ahejlsberg thought this up; I am merely providing the summary 😄
Use Cases
Many JavaScript libraries support taking in values of more than one type. For example, jQuery's AJAX settings object's jsonp
property can either be false
or a string
. TypeScript definition files have to represent this property as type any
, losing type safety.
Similarly, Angular's http service configuration (https://docs.angularjs.org/api/ng/service/$http#usage) has properties that of "either" types such as "boolean
or Cache
", or "number
or Promise
".
Current Workarounds
This shortcoming can often be worked around in function overloads, but there is no equivalent for object properties, type constraints, or other type positions.
Introduction
Syntax
The new |
operator, when used to separate two types, produces a union type representing a value which is of one of the input types.
Example:
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
Multiple types can be combined this way:
function process(n: string|HTMLElement|JQuery) { /* ... */ } process('foo'); // OK process($('div')); // OK process(42); // Error
Any type is a valid operand to the |
operator. Some examples and how they would be parsed:
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
Note that parentheses are not needed to disambiguate, so they are not supported.
Interpretation
The meaning of A|B
is a type that is either an A
or a B
. Notably, this is
different from a type that combines all the members of A
and B
. We'll explore
this more in samples later on.
Semantics
Basics
Some simple rules:
- Identity:
A|A
is equivalent toA
- Commutativity:
A|B
is equivalent toB|A
- Associativity:
(A|B)|C
is equivalent toA|(B|C)
- Subtype collapsing:
A|B
is equivalent toA
ifB
is a subtype ofA
Properties
The type A|B
has a property P
of type X|Y
if A
has a property P
of type X
and B
has a property P
of type Y
. These properties must either both be public, or must come from the same declaration site (as specified in the rules for private
/protected
). If either property is optional, the resulting property is also optional.
Example:
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((transport).type); // OK
Call and Construct Signatures
The type A|B
has a call signature F
if A
has a call signatureF
and B
has a call signature F
.
Example:
var t: string|boolean = /* ... */; console.log(t.toString()); // OK (both string and boolean have a toString method)
The same rule is applied to construct signatures.
Index Signatures
The type A|B
has an index signature [x: number]: T
or [x: string]: T
if both A
and B
have an index signature with that type.
Assignability and Subtyping
Here we describe assignability; subtyping is the same except that "is assignable to" is replaced with "is a subtype of".
The type S
is assignable to the type T1|T2
if S
is assignable to T1
or if S
is assignable to T2
.
Example:
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
The type S1|S2
is assignable to the type T
if both S1
and S2
are assignable to T
.
Example:
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
Combining the rules, the type S1|S2
is assignable to the type T1|T2
if S1
is assignable to T1
or T2
and S2
is assignable to T1
or T2
. More generally, every type on the right hand side of the assignment must be assignable to at least one type on the left.
Example:
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)
Best Common Type
The current Best Common Type algorithm (spec section 3.10) is only capable of producing a type that was among the candidates, or {}
. For example, the array [1, 2, "hello"]
is of type {}[]
. With the ability to represent union types, we can change the Best Common Type algorithm to produce a union type when presented with a set of candidates with no supertype.
Example:
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>
Note that in this case, the type Dog|Cat
is structurally equivalent to Animal
in terms of its members, but it would still be an error to try to assign a Rhino
to x[0]
because Rhino
is not assignable to Cat
or Dog
.
Best Common Type is used for several inferences in the language. In the cases of x || y
, z ? x : y
, and [x, y]
, the resulting type will be X | Y
(where X
is the type of x
and Y
is the type of y
). For function return statements and generic type inference, we will require that a supertype exist among the candidates.
Example
// 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 ... */ }
Possible Next Steps
Combining Types' Members
Other scenarios require a type constructed from A
and B
that has all members present in either type, rather than in both. Instead of adding new type syntax, we can represent this easily by removing the restriction that extends
clauses may not reference their declaration's type parameters.
Example:
interface HasFoo extends T { foo: string; } interface Point { x: number; y: number; } var p: HasFoo = /* ... */; console.log(p.foo); // OK console.log(p.x.toString(); // OK
Local Meanings of Union Types
For union types where an operand is a primitive, we could detect certain syntactic patterns and adjust the type of an identifier in conditional blocks.
Example:
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 }
This might also extend to membership checks:
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 }