Repository navigation
Spec Preview: Union types聽#805
Description
Activity
- addedSuggestionAn idea for TypeScriptAn idea for TypeScriptNeeds More InfoThe issue still hasn't been fully clarifiedThe issue still hasn't been fully clarifiedIn DiscussionNot yet reached consensusNot yet reached consensus
on Oct 2, 2014 If a property X exists in A or B but not both, the type A|B has an optional property X of type {} for the purposes of property access.Why
{}? It might be more logical to give it the type of the property X in A or B.Yay!!!!!! Been waiting for this!
I was a bit confused that it says:
Note that parentheses are not needed to disambiguate, so they are not supported.
And then subsequently lists a rule which features parantheses:
Associativity: (A|B)|C is equivalent to A|(B|C)
RyanCavanaugh commented
on Oct 2, 2014 MemberAuthorMore actionsWhy {}? It might be more logical to give it the type of the property X in A or B.
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:var x: Cat|Dog = /* ... */; // One of these lines is guaranteed to fail x.meow(); x.woof();
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.And then subsequently lists a rule which features paratheses:
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:
var x: string|number; var y: number|boolean; // a and b have the *identical* types string|number|boolean; the order of merging does not matter var a: typeof x|boolean; var b: string|typeof y;
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.
Best Common Type
Combining Types' MembersCool!!
Local Meanings of Union Types
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.
// make synonym, but it is not exists actual library code. declare var fooCommonReturnType: string | number; interface IFoo { bar(): typeof fooBarReturnType; buzz(): typeof fooBarReturnType; }but it is not smart.
I want to use union type with #229.I want to write the code for this image.
interface IFooCommonReturn { &this: string | number; } interface IFoo { bar(): IFooCommonReturn; buzz(): IFooCommonReturn; }DanielRosenwasser commented
on Oct 2, 2014 MemberMore actionsDo 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.Local Meanings of Union Types
An parentheis block
{meaning of any variable type in general would be good:var foo:number; if(true){ // Do some magic here to make foo a string so we don't need casting below: // I know someone said it was a number above ... but now I want to use it as a string var upper = foo.toUpperCase(); var lower = foo.toLowerCase(); }
馃憤
The intent is that you don't use foo as an A or a B until you've used a type assertion or other mechanism to actually "decide" which thing foo is.
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.
interface CanHaveXY { x?: number; y?: number; } interface HasX { x: number; } interface HasY { y: number; } var point1: HasX | HasY = ...; var point2: CanHaveXY = point1;
RyanCavanaugh commented
on Oct 2, 2014 MemberAuthorMore actions[incorrect response/example removed]
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:
var x: HasX = { x: 42, y: 'hello' }; // Forget about y var point2: CanHaveXY = x;
This is already allowed today.
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:
declare function choose<T>(x: T, y:T): T; var result = choose(1, "hm"); // today result is {}, with this it would be number|string
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
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 commented
on Oct 19, 2014 ContributorMore actionsSamuel Neff (@samuelneff) I think your specific example does not really require a union type, as we can still use
ConnectionPoolSettingsinifstatement:if (ConnectionPoolSettings) { /* ... */ }
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.
// W3C Web Cryptography API interface SubtleCrypto { encrypt( algorithm: string|Algorithm, key: Key, data: ArrayBuffer|ArrayBufferView); /* ... */ }
Anders Hejlsberg (@ahejlsberg), NN (@NN---) ,
The code
function add(name : "click"|"dblclick")is really asking for an enum that permits string values:enum ClickType { click = 'click', dblclick = 'dblclick' } function add(name: ClickType){} add(ClickType.click); // okay add('click'); // okay add('foo'); // error
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 commented
on Nov 15, 2014 More actionsPerhaps 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.DouglasLivingstone commented
on Nov 17, 2014 More actionsNoel Abrahams (@NoelAbrahams) nice, thanks!
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.
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:function saySize(message: number | number[]) { if (message instanceof Array) { return message.length; // Error } }
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.Arnav Singh (@Arnavion) thanks. I did try it with classes, the following does not work
class Message { value: string; } function saySize(message: Message | Message[]) { if (message instanceof Array) { return message.length; // test.ts(7,24): error TS2339: Property 'length' does not exist on type 'Message | Message[]'. } }
Not sure if its useful, the following works:
class Message { } function saySize(message: Message | Message[]) { if (message instanceof Array) { return message.length; // Okay } }
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.
Is it possible to have an interface extend a union type? For example:
interface Foo { foo: any; } interface Bar { bar: any; } interface FooBar extends Foo|Bar { fooBar: any; }
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):
interface IFooBar { fooBar: any; } type FooBar = (Foo | Bar) & IFooBar
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.
- locked and limited conversation to collaborators
on Jun 18, 2018
Updated 10/3 (see comment for changelog)
|operator for typesThis 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
jsonpproperty can either befalseor astring. TypeScript definition files have to represent this property as typeany, 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 "
booleanorCache", or "numberorPromise".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:
Multiple types can be combined this way:
Any type is a valid operand to the
|operator. Some examples and how they would be parsed:Note that parentheses are not needed to disambiguate, so they are not supported.
Interpretation
The meaning of
A|Bis a type that is either anAor aB. Notably, this isdifferent from a type that combines all the members of
AandB. We'll explorethis more in samples later on.
Semantics
Basics
Some simple rules:
A|Ais equivalent toAA|Bis equivalent toB|A(A|B)|Cis equivalent toA|(B|C)A|Bis equivalent toAifBis a subtype ofAProperties
The type
A|Bhas a propertyPof typeX|YifAhas a propertyPof typeXandBhas a propertyPof typeY. These properties must either both be public, or must come from the same declaration site (as specified in the rules forprivate/protected). If either property is optional, the resulting property is also optional.Example:
Call and Construct Signatures
The type
A|Bhas a call signatureFifAhas a call signatureFandBhas a call signatureF.Example:
The same rule is applied to construct signatures.
Index Signatures
The type
A|Bhas an index signature[x: number]: Tor[x: string]: Tif bothAandBhave 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
Sis assignable to the typeT1|T2ifSis assignable toT1or ifSis assignable toT2.Example:
The type
S1|S2is assignable to the typeTif bothS1andS2are assignable toT.Example:
Combining the rules, the type
S1|S2is assignable to the typeT1|T2ifS1is assignable toT1orT2andS2is assignable toT1orT2. More generally, every type on the right hand side of the assignment must be assignable to at least one type on the left.Example:
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:
Note that in this case, the type
Dog|Catis structurally equivalent toAnimalin terms of its members, but it would still be an error to try to assign aRhinotox[0]becauseRhinois not assignable toCatorDog.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 beX | Y(whereXis the type ofxandYis the type ofy). For function return statements and generic type inference, we will require that a supertype exist among the candidates.Example
Possible Next Steps
Combining Types' Members
Other scenarios require a type constructed from
AandBthat 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 thatextendsclauses may not reference their declaration's type parameters.Example:
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:
This might also extend to membership checks: