46
Vote

Add support for type unions

description

There are many new APIs that are being proposed as part of ES6 and DOM that are not easy to reproduce in a structurally typed language like TypeScript. For example, the proposed ES6 loader API (http://wiki.ecmascript.org/doku.php?id=harmony:module_loaders) contains a number of functions that expect one of two possible return types. Another example is the proposed Promise API (http://wiki.ecmascript.org/doku.php?id=strawman:promises or http://dom.spec.whatwg.org/#promises) that take a callback that is either a value (which would be a generic T), or a promise (which would be a generic Promise<T>).

In TypeScript, this can require a large number of overloads (in the case where the parameters to a method signature can differ) as in the Promise API, or is ambiguous (in the case where two similar method signatures only differ by the return type) as in the Loader API.

I'd like to see type annotations provide some support for a type union. For example:

promise
class Promise<T> {
  // ...
  static any<TResult>(...values: <Promise<T> | T>[]): Promise<T>;
  static every<TResult>(...values: <Promise<T> | T>[]): Promise<T[]>;
  static some<TResult>(...values: <Promise<T> | T>[]): Promise<T>;
  then(resolve?: (value: T) => <Promise<T> | T>, reject?: (value: any) => <Promise<T> | T>): Promise<T>;
  // ...
}
loader
class Loader<T> {
  // ...
  normalize(name: string, referer?: Referer): <string | { normalized: string; metadata?: any }>;
  resolve(normalized: string, options?: { referer: Referer; metadata: any; }): <string | { address: string; extra?: string[]; } >;
  // ...
}
When static type checking is performed, it is possible to have some type issues when explicitly providing generic type arguments and having the wrong type chosen, but this exists today without supporting type unions.

The other open issue is what to do with a local that is a type union: should it act as an any, a type that contains all of the members of both (or all) types in the union, or a type that only contains the members that are the same in both (or all) types in the union.

An implicit or explicit type cast from a type union to one of the union types would pass without the need to perform an intermediate cast to <any>, and an explicit type cast to a more specific type for any of the union types would likewise succeed without an intermediate cast.

Assignment to a local or field that is a type union would succeed as if it were either of the types (e.g. implicit or explicit type cast from a more specific type to a less specific type specified in the type union).

There is also a question on how to properly handle the intellisense for a symbol that uses a type union. It could either be represented as a number of overloads (similar to what would have to be typed today), or preserve the type union definition.

Providing a typedef-like syntax for unions could also be useful:
union Ref<T> {
  Promise<T>;
  T;
}
Also, some possible ideas for the syntax of a type union are provided below:
// either A or B, using parenthesis
var a: (A | B);

// either A or B, using angle brackets (similar to a type-cast or generic type argument list)
var a: <A | B>;

// either A or B using an explicit keyword 'union', mirrors the proposed typedef-like syntax
var a: union { A; B; }; 

// either A or B using an explicit keyword 'union', mirroring normal generic type signatures
var a: union<A, B>;

comments

danquirk wrote Jul 19, 2013 at 6:14 PM

Thanks for the suggestion. This is something we've considered in the past and may revisit in the future. Assigned to Jonathan.

yln wrote Aug 6, 2013 at 8:11 AM

this is a great idea.Because lot variables will behave at run time depending on the type.

AdamFreidin wrote Sep 13, 2013 at 8:00 PM

I think the following is also possible:
var a: number|string|function = 3;

basarat wrote Sep 29, 2013 at 7:22 AM

Not clear why they cannot be supported the same way function overloading is:
// Union types not allowed
var foo:{
    x:number;
    x:string;
    x:any; 
}
// Overloading in functions is allowed 
function bar(x:number);
function bar(x:string);
function bar(x:any){}
However I very much prefer the following proposed syntax:
// Proposed syntax: 
var foo{
    x:number|string;
} 
function bar(x:number|string);
As it can even give us further restrictions:
// The following should be an error: 
bar(true); // Not a number or a string 
There are a lot of definitions on DefinitelyTyped where the options bags are littered with any only because of lack of this support. e.g. Jquery UI
  interface AccordionOptions {
        active?: any; // boolean or number
But there is simply no way to enforce that type safety right now without this feature. And eventually people need to look at the type definition to find out what is allowed when the compiler could easily enforce it.

jonturner wrote Oct 29, 2013 at 5:16 PM

Union types add complexity to type inference and tooling. For example:
function bar(x: number|string) {
  x.  // <-- what do we show here?
}
The completion list for the above, should it show all of the members of both number or string, the interaction, or only one of them?

I do think they add value, and I'm sure we can find reasonable solutions to some of the engineering questions, but I'm not sure if the added complexity to the type system, type inference, and tooling is worth the benefit of slightly better type safety.

In the initial Promise example, I believe those can be written today using overloads. I'd like to see a strong motivating example for union types that tip the scales and show that it's worth the added complexity.

MgSam wrote Oct 29, 2013 at 5:36 PM

@jonturner Overloads definitely do not solve the issue. Look at some of the definition files on Definitely Typed and you will see tons of anys all over the place (JQuery anyone?), and it's almost entirely because of the lack of type unions.

The completion list issue is simple to solve- only show the members common to both types. Doing anything else would violate the type safety TypeScript works to achieve. If the user needs to access members unique to the type, they have to cast it.

IMO, if TypeScript never gets type unions it will fall far short of its goal of fully describing the JavaScript type system.

georgiosd wrote Oct 29, 2013 at 9:52 PM

I'll +1 MgSam's comments.

AngularJS severely needs union to have a complete TS definition.

Example:

With reference
var directive: ng.IDirective = { 
   controller: 'name of controller'
 };
or inline
controller: function(injectedServiceA) {
}
or minify-safe array notation
controller: ['injectedServiceA', function(injectedServiceA) { }]
It's full of these. Another example, also in ng.IDirective
require: 'single string'
or
require: ['string1', 'string2']
Keep the tooling simple if you must, it's better than using any after all

basarat wrote Oct 29, 2013 at 10:25 PM

I don't think there are any js projects of significant size that don't need union types for their option bags.

basarat wrote Oct 29, 2013 at 10:35 PM

basarat wrote Oct 30, 2013 at 12:44 AM

basarat wrote Nov 8, 2013 at 7:32 AM

basarat wrote Nov 15, 2013 at 4:19 PM

pasnell wrote Nov 20, 2013 at 3:41 AM

Continuing here from twitter - I also think this is an important part of JavaScript's type system and should be supported in TypeScript. I'd also like to offer some suggestions for some concerns that have been brought up here.

Regarding syntax, I prefer this if possible, because of simplicity and clarity:
var x:number|string;
I also think there should be some sort of syntax to name a type union. I think the word union is a good fit, although it would be nice to not have to introduce another keyword. My preference on syntax would be the following:
union Server = http.Server | myApp.Server;
There are a couple big problems with overloads right now.
  • They only work with function parameters. As others pointed out above, there are several cases where the lack of type unions for variables makes type safety impossible.
  • Type safety with overloads is only enforced when calling a function. This works well for libraries written in JS, but there should be a way of guaranteeing type safety while authoring a function in TypeScript. This can be fixed, but in order to do it properly, you're basically introducing the type union concept anyways.
Here is an example that shows how type safety can be broken. In a large project, this is the type of mistake that could be easy to make but hard to track down, which TypeScript is supposed to prevent, but currently does not.
class A {
  x: number = 0;
  y: string = 'g not called yet';
}

class B {
  x: boolean = false;
  y: string = 'g not called yet';
}

// f accepts A|number
function f(value: A);
function f(value: number);
function f(value: any) {
  // This code looks safe based on how I've defined f.
  if (typeof value === 'number') {
    console.log(value);
  } else {
    // It must be A, based on the definition of f.
    console.log(value.x);
    value.x = -1; // reset it to some value.
  }
}

// g accepts A|B
function g(value: A);
function g(value: B);
function g(value: any) {
  value.y = 'value of type ' + value.constructor.name + ' passed to g().';
  // Oops, I forgot that value can't be type B when calling f().
  f(value);
}

var a = new A;
var b = new B;

console.log(a.y);
console.log(a.x);

console.log(b.y);
console.log(b.x);

g(a);
g(b);

console.log(a.y);
console.log(a.x);

console.log(b.y);
console.log(b.x); // TypeScript thinks this is a Number, but it is now a Boolean.
If a type union concept existed, I would imagine that type A could be passed as type A|number, but type A|B could not. I imagine you would need to explicitly cast type A|B, which would make you think about writing some checks, like I did in the f function, but "forgot" in g.

The last thing I'd like to suggest is, as in my example, if all types in a type union share a property with the same name and type, it should be accessible without casting. So in my example, in function g, value.y should be accessible without a cast, because it is always the same type, but value would need to be cast to A or B before its x property can be accessed. So we are essentially creating a type that is the intersection of all the members of the types in the union. This also answers the question of what the IDE should suggest when you write value..

I hope this helps in the decision making process, and I'd be glad to answer any questions or discuss this feature more in depth.

mwisnicki wrote Nov 20, 2013 at 9:06 AM

Speaking of syntax, rather than union keyword I would prefer generalized type aliasing:
type Server = http.Server | myApp.Server;
type Z = x.y.Z;
and maybe even
type IFooBar = IFoo & IBar;

basarat wrote Feb 20 at 9:40 PM

Also needed for requireJS where require can be either Require or RequireConfig. See : https://github.com/borisyankov/DefinitelyTyped/issues/1716

johnny_reilly wrote Feb 21 at 8:07 AM

Hi @jonturner

I'd say there's a lot of evidence of use cases for type unions; mainly to support the creation of type definitions for existing JS libraries.

Since this issue is still only at "proposed" it's clearly not going to be part of TS v1 but I'd love to hear if you guys are thinking of working on it after that? Would you be able to give an update on what the TypeScript teams thoughts are on supporting type unions?

stijnherreman wrote Mar 6 at 1:30 PM

Any news on this?

MgSam wrote Mar 6 at 2:01 PM

@stijnherreman I wouldn't count on any for quite some time. They're not even going to consider new features until after the 1.0 release which could be a while away still given all the bug reports about 0.9.7.

Bartvds wrote Apr 7 at 12:27 PM

We're on to 1.0 so time for a +1 and a re-evaluation? Stuff like this keeps coming by at DT.

Worst-case I'm aware of is an overload with 8 overloads (!!) for bluebird, it is a bit crazy.

johnny_reilly wrote Apr 7 at 2:25 PM

As a fellow DT contributor I'd +100 this if I could! I'm far more interested in this than async await etc because I see this as one of the weak parts of TypeScript at present. The lack of this feature leads to an absence of useful type information in many typing files (since the any type is not much of a guide as to the actually accepted types)