71
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:

ES6 Promise

class Promise<T> {
  // ...
  static any<TResult>(...values: union { Promise<T>; T; }[]): Promise<T>;
  static every<TResult>(...values: union { Promise<T>; T; }[]): Promise<T[]>;
  static some<TResult>(...values: union { Promise<T>; T; }[]): Promise<T>;
  then(resolve?: (value: T) => union { Promise<T>; T; }, reject?: (value: any) => union { Promise<T>; T; }): Promise<T>;
  // ...
}

ES6 Loader

class Loader<T> {
  // ...
  normalize(name: string, referer?: Referer): union { string; { normalized: string; metadata?: any }; };
  resolve(normalized: string, options?: { referer: Referer; metadata: any; }): union { 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.

Union Declaration

Providing a typedef-like syntax for unions would also be useful to define a reusable definition:
union Ref<T> {
  Promise<T>;
  T;
}
This is roughly analogous to an interface that defines multiple call signatures:
// as an interface with call signatures...
interface ResolveCallback<T> {
  (value: Promise<T>): void;
  (value: T): void;
  (): void;
}

// ...and as a union with function types
union ResolveCallback<T> {
  (value: Promise<T>) => void;
  (value: T) => void;
  () => void;
}

Static Analysis

Adding type unions would require changes to the static type information to be supported. The primary goal of adding type unions is to help the compiler determine the best matching type for a call expression or return type expression. The following sections discuss various ways of handling static analysis of type unions.

Assigning to a Type Union

When assigning to an identifier that is annotated with a type union, passing a value as an argument to a function for a parameter that is a type union, returning a value from a function with a type union in its return type annotation, or type-casting a value to a type union, the type of the value being assigned or returned must be compatible (as either an exact match or a superset of type information) with one of the types defined in the type union.

For example:
// assign to variable
var value: union { Promise<number>; number; };
value = 1; // legal
value = Promise.resolve<number>(1); // legal
value = new Date(); // static error

// type-cast to union
declare var n: number;
declare var p: Promise<number>;
declare var a: any;
declare var d: Date;
<union { Promise<number>; number; }>n; // legal
<union { Promise<number>; number; }>p; // legal
<union { Promise<number>; number; }>a; // legal
<union { Promise<number>; number; }>d; // legal

// union in return value
function func(type: string): union { Promise<number>; number; } {
  switch (type) {
    case "number":
      return 1; // legal

    case "promise":
     return Promise.resolve<number>(1); // legal

    case "date":
     return new Date(); // static error  
  }
}

// union in invocation expression
declare function func(promiseOrValue: union { Promise<number>; number; }): void;
declare var n: number;
declare var p: Promise<number>;
declare var a: any;
declare var d: Date;

func(n); // legal
func(p); // legal
func(a); // legal
func(d); // static error

Assigning from a Type Union

When assigning to another value from a type union or type-casting from a type union, the type of the value must be compatible (as either an exact match or a subset of type information) with one of the types in the union.

For example:
// assignment
declare var value: union { Promise<number>; number; };
var n: number;
var p: Promise<number>;
var a: any;
var d: Date;

n = value; // legal
p = value; // legal
a = value; // legal
d = value; // static error

// type-cast
<Promise<number>>value; // legal
<number>value; // legal
<any>value; // legal
<Date>value; // static error

Member Access or Indexers on a Type Union

When a value that is a type union is part of either a member access or indexer expression, it is treated as any, and would only return a static error if --noImplicitAny is set.

Type Unions and Intellisense

The easiest approach to handling Intellisense on a value that is a type union is to treat it as any. To get more specific type information would require a type-cast to a more specific type.

Type Unions and Heritage clauses

A Type Union can only be used in a type annotation and cannot be used in the position of a heritage clause in a class or interface (e.g. it cannot follow an extends or implements clause).

Type Unions and Generics

A Type Union can contain a generic type parameter. When a best matching type is determined, the types are compared in definition order from top to bottom, as with overloads. For example:
// this...
declare function resolve<T>(value: union: { Promise<T>; T; }): Promise<T>;

// is equivalent to this...
declare function resolve<T>(value: Promise<T>): Promise<T>;
declare function resolve<T>(value: T): Promise<T>;

Type Unions and Generic Constraints

A Type Union can be used as part of a generic constraint. For example:
declare function log<T extends union { string; Message; }>(message: T): void;

Type Unions and Recursive Generic Types

One of the goals for this proposal was to provide better typing for ES6 JavaScript built-ins, such as Promise. There is a problem with this proposal, however. It fails to address a quirk of ES6 Promises in that it is possible to have static type information for a recursively-nested Promise (i.e. Promise<Promise<T>>), while ES6 Promises adopt the state of any Promise passed to their resolution procedure (e.g., it is not possible to have a Promise for a Promise). This represents a possible foot-gun for users where they could provide explicit static typing for a Promise that cannot match the actual state of the promise.

One solution to this is to add an additional not keyword to the heritage clause of a generic constraint. This would allow you to provide an exclusion list of types that cannot be used as part of the parameter. For example, this would be the type declaration for an ES6 Promise that supports its actual operation:
declare class Promise<TValue not Promise<any>> {
  constructor(init: (resolve: (value?: union { Promise<TValue>; TValue; }) => void, reject: (reason: any) => void);

  then<TResult not Promise<any>>(
    resolve?: (value: TValue) => union { Promise<TResult>; TResult; },
    reject?: (value: any) => union { Promise<TResult>; TResult; }
  ): Promise<TResult>;

  catch(
    reject: (value: any) => union { Promise<TValue>; TValue; }
  ): Promise<TValue>;

  resolve<T not Promise<any>>(value: union { Promise<T>; T; }): Promise<T>;
  resolve(): Promise<void>;

  reject<T not Promise<any>>(error: any): Promise<T>;
  reject(reason: any): Promise<void>;

  all<T not Promise<any>>(values: union { Promise<T>; T; }[]): Promise<T[]>;
  race<T not Promise<any>>(values: union { Promise<T>; T; }[]): Promise<T>;
}
Edit: Switched to union syntax for proposal, removed other alternatives, added more depth into how to handle (at least some of) the static analysis and intellisense issues, and added a proposed not constraint to generic constraints for better typing of Promise.

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)

dlpowell wrote May 19 at 10:25 AM

I just ran across a use case for this while configuring Durandal. The routes in durandal can be configured like so:

export function activate() {
return router_.map([
    { route: ['', 'search'], moduleId: 'viewmodels/search', title: 'Search', nav: true },
    { route: 'handsFree', moduleId: 'viewmodels/handsFree', title: 'Hands Free', nav: true }
]).buildNavigationModel()
    .activate();
}

This allows the default/empty route "" to be mapped to the viewmodels/search module. The objects being created are instances of DurandalRouteConfiguration which defines the route parameter as "route?: string". Therefore, the search route is invalid because the route parameter we're supplying is an array. In this case, it would be useful to define route as "route?: string | string[]".

juandopazo wrote May 24 at 3:41 PM

I like this idea. In Haskell you can define a type by enumerating other types:
data Bool = False | True
Should the same ability be added to interfaces in TypeScript so as not to add another keyword? Maybe (referencing the Loader issue with promises)...?
interface Eventual<T> = T | Promise<T>;
About the issue with code completion, should it be disabled until the programmer dealt with the type ambiguity?

juandopazo wrote May 24 at 4:05 PM

Also note that enumerated types as in interface Foo = Bar also allow for much more natural definitions of function types:
interface MyFun = (string) => boolean;

// vs

interface MyFun {
  (string): boolean;
}

jamesnw wrote May 25 at 1:19 AM

I would imagine a better method would be to support existing semantics for extending interfaces.
interface Eventual<T> extends T | Promise<T>;
interface MyFun extends (string) => boolean;
And by "Better", I mean "easier to support quickly" (interfaces are not objects/values like modules and classes, so I'm guessing '=' would not work in the design). IMHO just removing the need to declare a body for types may be all that is needed, such as my request here: https://typescript.codeplex.com/workitem/2475

SamuelR wrote Jun 13 at 1:50 PM

I ran into the need for union types a lot when creating a definition for Sequelize.

https://github.com/samuelneff/DefinitelyTyped/blob/sequelize/sequelize/sequelize.d.ts

There are a lot of options objects that take a property which can be a string or a more elaborate object.

Thanks,

Sam

ScottMcArthur wrote Jun 29 at 10:06 AM

We definitely need this. Without this support, currently using any in the numerous projects type definitions that use option bags, including jQuery and AngularJS, which makes the whole practise of defining types at all pointless.

It's almost a year since this issue was raised and the practise of using option bags with overloading of the accepted type is becoming more prevalent in JavaScript projects, with still no support from TypeScript. Is there any feedback on this?

SaschaNaz wrote Jun 30 at 4:24 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?
How about this way? I sometimes want this when I make overloads.
function bar(x: number|string) {
  if (typeof x === "number") {
    declare x: number; // Let's declare x as number in this block
    x. // Now we know what to show
  }
  else {
  }
}

rbuckton wrote Jun 30 at 7:38 PM

Edit: Switched to union syntax for proposal, removed other alternatives, added more depth into how to handle (at least some of) the static analysis and Intellisense issues, and added a proposed not constraint to generic constraints for better typing of Promise.

rbuckton wrote Jun 30 at 7:49 PM

jonturner wrote:

Union types add complexity to type inference and tooling. For example:
[snip]
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've amended the suggestion with a possible approach to handling type inference. Generally, I would treat any member access/indexer as any and allow rules such as --noImplicitAny to catch errors. If you want more specific typing, you would type cast (which you would have to do with any today anyways).

There are three major values of type unions (and the not constraint as defined above):
  • Ability to limit the return value of a function/method to a restricted list of allowed types (which supports Promise, Loader, etc). This is not possible today in TypeScript (other than using any).
  • Ability to properly infer a generic type parameter for a callback when there are multiple possible return values. This is only partly possible today in TypeScript using multiple overloads, and still only if the function supplied to the callback only returns one of those types.
  • Ability to properly type complex ES6 built-ins like Promise, which do things like adopt promise state when resolving with a promise). There are a lot of cases where this breaks using TypeScript today.

omidkrad wrote Jul 2 at 11:07 PM

Until union types are supported by TypeScript compiler, let's promote a convention of commenting those not-so-any types. My proposal is like below, borrowing partially from Closure Compiler annotations.
interface DatepickerOptions {
    /**
     * Beginning of time.
     * @type {Date} | {string}
     */
    startDate?: any;

    /**
     * End of time.
     * @type {Date} | {string}
     */
    endDate?: any;
}

mwisnicki wrote Jul 2 at 11:47 PM

Better yet, use actual closure/jsdoc syntax: @type {(Data|string)}

basarat wrote Jul 2 at 11:47 PM

@omidkrad looks good:
Image

basarat wrote Jul 3 at 12:22 AM

@omidkrad : @mwisnicki 's syntax is better : http://usejsdoc.org/tags-type.html (see "type union")

In either case TS doesn't care about @types and just displays it as a part of the jsdoc string.

I wonder if its just easier to go with @type Date|string

ie :
interface DatepickerOptions {
/**
* Beginning of time.
* @type Date|string
*/
startDate?: any;

/**
* End of time.
* @type Date|string
*/
endDate?: any;
}
Its easier to type / update.

Image

omidkrad wrote Jul 3 at 9:31 PM

Thank you @mwisnicki and @basarat. I'd go with Closure Compiler's syntax just because there is a spec for it. I like using a well defined annotation syntax over a simple comment because if we ever get TypeScript support for union types, then it would be possible to develop tools to help with the conversion.

ollehar wrote Jul 7 at 2:02 PM

Why on earth would it be legal to typecast a Date variable to a union, as in the example above (and below):
declare var d: Date;
<union { Promise<number>; number; }>d; // legal
Shouldn't there be static checking on typecasts, making sure you only typecast in the right direction in the object hierarchy? That is, to a more general type.

masonk wrote Jul 17 at 3:10 AM

+1 for @mwisnicki's generalization. Two great features for the price of one keyword!

SaschaNaz wrote Jul 21 at 8:17 AM

I agree with @ollehar, I think <union { Promise<number>; number; }><any>d; should be possible but not <union { Promise<number>; number; }>d;.