Repository navigation
Spec Preview: Union types聽#805
Description
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". Anders Hejlsberg (@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|stringMultiple types can be combined this way:
function process(n: string|HTMLElement|JQuery) { /* ... */ }
process('foo'); // OK
process($('div')); // OK
process(42); // ErrorAny 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 numberNote 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|Ais equivalent toA - Commutativity:
A|Bis equivalent toB|A - Associativity:
(A|B)|Cis equivalent toA|(B|C) - Subtype collapsing:
A|Bis equivalent toAifBis 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((<Car>transport).type); // OKCall and Construct Signatures
The type A|B has a call signature F if A has a call signature
F 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 numberThe 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 numberCombining 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<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(); // OKLocal 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
}
Activity
ivogabe commentedon Oct 2, 2014
Why
{}? It might be more logical to give it the type of the property X in A or B.johnnyreilly commentedon Oct 2, 2014
Yay!!!!!! Been waiting for this!
I was a bit confused that it says:
And then subsequently lists a rule which features parantheses:
RyanCavanaugh commentedon Oct 2, 2014
The intent is that you don't use
fooas anAor aBuntil you've used a type assertion or other mechanism to actually "decide" which thingfoois. If we jammed on all the properties ofAandB, you'd have a sort of nonsense object -- imagine code like this:The other option on the table is to not have those properties at all, but there's concern that this makes code like
if(x.meow) { /* x is Cat */ }too annoying to write.I couldn't come up with a more clear way to write this rule; the parens here are just for explanatory purposes. Consider code like this:
johnnyreilly commentedon Oct 2, 2014
Thanks for the clarification Ryan Cavanaugh (@RyanCavanaugh). I'm trying to think of scenarios where lack of parens would be a problem - instinctively I'm assuming there must be some! But it's first thing in the morning and I haven't had coffee yet... - I'm sure you guys covered that off.
I really like the "Local Meanings of Union Types" possible next step which adjust the type of an identifier in conditional blocks. I think this would be really useful. That said I think the rules that govern how this works need to be very clear. I'm also curious about the IDE experience - would hovering over the identifier in a conditional block reveal it as, for example, a Dog or a Cat|Dog. I'm hoping for the specific type rather than the union in this scenario.
vvakame commentedon Oct 2, 2014
Cool!!
please add
instanceofto rule. 馃槈and, I have a one question.
How do I can make type synonym for union types?
I came up with a hack of one.
but it is not smart.
I want to use union type with #229.
I want to write the code for this image.
DanielRosenwasser commentedon Oct 2, 2014
Do we have a special case for
void | T? Shouldvoid | Tend up beingT, or is it helpful to maintain thevoid? I can see this as both useful as well as something that might turn out to be an anti-pattern.basarat commentedon Oct 2, 2014
An parentheis block
{meaning of any variable type in general would be good:samuelneff commentedon Oct 2, 2014
馃憤
ivogabe commentedon Oct 2, 2014
That sounds like a valid reason to me. But would this be allowed? My opinion would be yes, but following these rules it would be disallowed.
RyanCavanaugh commentedon Oct 2, 2014
[incorrect response/example removed]
ahejlsberg commentedon Oct 2, 2014
Ivo Gabe de Wolff (@ivogabe) It would be allowed. The proposed rule is that A|B is assignable to T if A and B are both assignable to T, and they would be in the given example.
Ryan Cavanaugh (@RyanCavanaugh) The issue you call out really has nothing to do with union types. Consider:
This is already allowed today.
danquirk commentedon Oct 2, 2014
Another issue we talked about is the effect on generic type argument inference if we change best common type to return unions rather than {}. Consider:
We'd previously considered adding an option to make it an error if type argument inference returns {}, we may just do the same thing here and make it an error for type argument inference to infer a union type unless that type exactly matches one of the candidate types. If anyone has specific uses for type argument inference to generate a union type that could be interesting.
46 remaining items
samuelneff commentedon Oct 19, 2014
Kagami Sascha Rosylight (@saschanaz) yes, if you're desiging your own library, but when you're writing definitions for an existing library, you don't have that choice, you have to write types that represent how the third-party library actually works, not how you wish it worked.
saschanaz commentedon Oct 19, 2014
Samuel Neff (@samuelneff) I think your specific example does not really require a union type, as we can still use
ConnectionPoolSettingsinifstatement:However, I agree with your point. We write types for existing codes (or even future ones), and many of them need union types to be correctly represented.
NoelAbrahams commentedon Oct 19, 2014
Anders Hejlsberg (@ahejlsberg), NN (@NN---) ,
The code
function add(name : "click"|"dblclick")is really asking for an enum that permits string values:NN--- commentedon Oct 19, 2014
Noel Abrahams (@NoelAbrahams) In this specific case enum string is possible solution.
It is really needed feature.
My intention is for more complicates cases with unrelated types like said before.
DouglasLivingstone commentedon Nov 15, 2014
Perhaps this is too out-there, but if
nullwas allowed as a type, it might be possible to redefinestringasSTRING|null, whereSTRINGis a hypothetical non-nullable string, which could be used to check that, e.g.,foo.bar.lengthhas aSTRINGbar, never anull.NoelAbrahams commentedon Nov 16, 2014
Douglas Livingstone (@DouglasLivingstone) see #185
DouglasLivingstone commentedon Nov 17, 2014
Noel Abrahams (@NoelAbrahams) nice, thanks!
jtheisen commentedon Nov 23, 2014
Ryan Cavanaugh (@RyanCavanaugh) "Disjoint properties are not present for the purposes of property access."
That's very good, I hope that's how it stays. I was quite disturbed when I read the quoted bit of the first comment here. The disjoint type shouldn't have anything the only one of the summands have.
This is also important for something like intellisense, where you really don't want to have those non-properties listed.
This is an awesome feature. It makes TypeScript the first type-safe real-world imperative language with that kind of power in a type system.
basarat commentedon Jan 2, 2015
What is the type guard syntax for
array. I don't seem to find that in this thread. For example I get an error with the latest compiler on below:Arnavion commentedon Jan 2, 2015
Basarat Ali Syed (@basarat) You should also be getting "error TS2358: The left-hand side of an 'instanceof' expression must be of type 'any', an object type or a type parameter." on the previous line. Because of that it's not functioning as a type guard and the type isn't being narrowed inside the if block.
Using typeof instead of instanceof works:
if (typeof message === "object") {Edit: Note that this is only a problem because a primitive is one of the members of the union. If you had a class C and message was declared as having type
C | number[]then bothinstanceof Candinstanceof Arraywould work.basarat commentedon Jan 4, 2015
Arnav Singh (@Arnavion) thanks. I did try it with classes, the following does not work
Not sure if its useful, the following works:
Arnavion commentedon Jan 4, 2015
Hmm, you're right. When I tested successfully with
message: C | number[]andmessage instanceof Array, I did use an empty class for C. Adding a member to that class causesmessage instanceof Arrayto fail to narrow again.Maybe open a new issue.
eggers commentedon Oct 28, 2015
Is it possible to have an interface extend a union type? For example:
I came across an issue with an DefinitelyTyped interface that has all optional params, so something like
numberwill satisfy the interface, and won't result in a compile error. In reality, most properties are optional, but eitheruriorurlmust be specified. (The interface is request.Options is anyone is curious)I'm aware that I could do the below, but then you FooBar isn't an interface anymore, and you can't further extend it (like request-promise does):
mhegazy commentedon Oct 28, 2015
Jacob Eggers (@eggers) you can only use an object type (interface or class) in an extends clause. Also in the future, I would file these as a new issue instead of commenting on an outdated issue.