-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Spec Preview: Union types #805
Comments
Why |
Yay!!!!!! Been waiting for this! I was a bit confused that it says:
And then subsequently lists a rule which features parantheses:
|
The intent is that you don't use 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
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 @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. |
Cool!!
please add 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 write the code for this image.
|
Do we have a special case for |
An parentheis block 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();
} |
👍 |
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; |
[incorrect response/example removed] |
@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. @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. |
@DanielRosenwasser |
👍 |
@danquirk We definitely want to change best common type to be a union of the constituent types (which may collapse to a single type if one of the types is a supertype of all others). Type inference produces a type that is the best common type of the candidates, and that type will be a union type in cases where multiple unrelated types are candidates. I don't see how we can make it an error to infer a union type. Consider: interface Animal {
name: string;
}
interface Giraffe extends Animal {
giraffeness: string;
}
interface Walrus extends Animal {
walrusness: string;
}
function makePair<T>(a: T, b: T) {
return [a, b];
}
var giraffe: Giraffe;
var walrus: Walrus;
var pair = makePair(giraffe, walrus); // Array<Giraffe|Walrus>
var animals: Animal[] = pair; This example fails without union types because pair is inferred to be |
The argument is that that example is an error. If I wanted to allow declare function makePair<T, U>(a: T, b: U): Array<T|U>; The version with a single type parameter is a subtype that is explicitly designed to only handle the case where both parameters are the same type and otherwise is an error. I don't think people write the single type argument case expecting it to allow different types for each argument. I do think people frequently use/write functional style combinators expecting an error if the provided list and predicate/selector are of incompatible types: declare function isEmpty(s: string): boolean;
declare function map<T,U>(a: T[], f: (n:T) => U): U[];
var result = map([1,2,3], isEmpty); // result is boolean[], no error Today we give no error here which is pretty bad. It's trivial to hit this kind of issue using underscore.d.ts and similar utility libraries. Currently you can only get safety here by using contextually typed lambas for your function typed arguments. I thought the frequency with which this has been an issue for us and users had led us to converging on a consensus that #360 was a good idea. |
But they're not of different types. They're both Animals, it just so happens that neither is a subtype of the other. This is not just a made up scenario, I run into this with regularity with the In our offline discussion about the I do understand that sometimes a union type inference is not what you want, but I don't think it is always the case. I think this proposed change just trades one problem for another. |
I've logged #810 to track the suggestion from yesterday about only using function parameter types if there are no other inference candidates. I'll also be referencing #360 a bunch in this comment (Issue warning when generic type inference produces We've already split the baby, so to speak, by having different rules for The question is simply which half of the baby generic type inference should end up in if we have union types - it's a heuristic question, and one that we should answer by looking at how generics are used in the wild. I looked through JQuery and Knockout, minus Promises because they're too verbose, to look at where they used generics where the type parameter was consumed more than once somewhere in the parameter list. A rough scan of underscore shows approximately the same set of functions, though its definition file is a mess. I've grouped these functions according to their intended behavior from a programmer's perspective.
We've discussed this family at length. Obviously,
A call of the form
These functions at least return
It's possible that you want
You don't want
This function mutates the underlying array, so forming a union type in the case of The only function that I see as being improved by creating union types ( |
Union types is a good idea. |
Updates from 10/3 design meeting:
|
Regarding _Local Meanings of Union Types_, it is fairly common to return either a type T or an array of type T. function foo(): string[]|string {
// ...
}
var result = foo();
if( Array.isArray(result)){
result.map(item => item.replace(...));
}
else {
result.replace(...);
} Another alternative is |
Perhaps this is too out-there, but if |
@NoelAbrahams nice, thanks! |
@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 function saySize(message: number | number[]) {
if (message instanceof Array) {
return message.length; // Error
}
} |
@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: 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 |
@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 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 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 |
@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. |
Updated 10/3 (see comment for changelog)
|
operator for typesThis 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 befalse
or 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 "
boolean
orCache
", or "number
orPromise
".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|B
is a type that is either anA
or aB
. Notably, this isdifferent from a type that combines all the members of
A
andB
. We'll explorethis more in samples later on.
Semantics
Basics
Some simple rules:
A|A
is equivalent toA
A|B
is equivalent toB|A
(A|B)|C
is equivalent toA|(B|C)
A|B
is equivalent toA
ifB
is a subtype ofA
Properties
The type
A|B
has a propertyP
of typeX|Y
ifA
has a propertyP
of typeX
andB
has a propertyP
of 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|B
has a call signatureF
ifA
has a call signatureF
andB
has a call signatureF
.Example:
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 bothA
andB
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 typeT1|T2
ifS
is assignable toT1
or ifS
is assignable toT2
.Example:
The type
S1|S2
is assignable to the typeT
if bothS1
andS2
are assignable toT
.Example:
Combining the rules, the type
S1|S2
is assignable to the typeT1|T2
ifS1
is assignable toT1
orT2
andS2
is assignable toT1
orT2
. 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|Cat
is structurally equivalent toAnimal
in terms of its members, but it would still be an error to try to assign aRhino
tox[0]
becauseRhino
is not assignable toCat
orDog
.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
(whereX
is the type ofx
andY
is 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
A
andB
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 thatextends
clauses 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:
The text was updated successfully, but these errors were encountered: