Skip to content

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|string

Multiple types can be combined this way:

function process(n: string|HTMLElement|JQuery) { /* ... */ }
process('foo'); // OK
process($('div')); // OK
process(42); // Error

Any 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 number

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 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|A is equivalent to A
  • Commutativity: A|B is equivalent to B|A
  • Associativity: (A|B)|C is equivalent to A|(B|C)
  • Subtype collapsing: A|B is equivalent to A if B is a subtype of A

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); // OK

Call 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 number

The 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 number

Combining 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(); // OK

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:

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

  1. ivogabe commented on Oct 2, 2014

    @ivogabe
    Contributor
    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.

  2. johnnyreilly commented on Oct 2, 2014

    @johnnyreilly

    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)

  3. RyanCavanaugh commented on Oct 2, 2014

    @RyanCavanaugh
    MemberAuthor

    Why {}? 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 foo as an A or a B until you've used a type assertion or other mechanism to actually "decide" which thing foo is. If we jammed on all the properties of A and B, 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;
  4. johnnyreilly commented on Oct 2, 2014

    @johnnyreilly

    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.

  5. vvakame commented on Oct 2, 2014

    @vvakame
    Contributor

    Best Common Type
    Combining Types' Members

    Cool!!

    Local Meanings of Union Types

    please add instanceof to 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;
    }
    
  6. DanielRosenwasser commented on Oct 2, 2014

    @DanielRosenwasser
    Member

    Do we have a special case for void | T? Should void | T end up being T, or is it helpful to maintain the void? I can see this as both useful as well as something that might turn out to be an anti-pattern.

  7. basarat commented on Oct 2, 2014

    @basarat
    Contributor

    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();
    }
  8. samuelneff commented on Oct 2, 2014

    @samuelneff

    馃憤

  9. ivogabe commented on Oct 2, 2014

    @ivogabe
    Contributor

    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;
  10. RyanCavanaugh commented on Oct 2, 2014

    @RyanCavanaugh
    MemberAuthor

    [incorrect response/example removed]

  11. ahejlsberg commented on Oct 2, 2014

    @ahejlsberg
    Member

    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.

  12. danquirk commented on Oct 2, 2014

    @danquirk
    Member

    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.

  13. 46 remaining items

  14. samuelneff commented on Oct 19, 2014

    @samuelneff

    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.

  15. saschanaz commented on Oct 19, 2014

    @saschanaz
    Contributor

    Samuel Neff (@samuelneff) I think your specific example does not really require a union type, as we can still use ConnectionPoolSettings in if statement:

    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);
      /* ... */
    }
  16. NoelAbrahams commented on Oct 19, 2014

    @NoelAbrahams

    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
  17. NN--- commented on Oct 19, 2014

    @NN---

    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.

  18. DouglasLivingstone commented on Nov 15, 2014

    @DouglasLivingstone

    Perhaps this is too out-there, but if null was allowed as a type, it might be possible to redefine string as STRING|null, where STRING is a hypothetical non-nullable string, which could be used to check that, e.g., foo.bar.length has a STRING bar, never a null.

  19. DouglasLivingstone commented on Nov 17, 2014

    @DouglasLivingstone
  20. jtheisen commented on Nov 23, 2014

    @jtheisen

    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.

  21. basarat commented on Jan 2, 2015

    @basarat
    Contributor

    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 
      }
    }
  22. Arnavion commented on Jan 2, 2015

    @Arnavion
    Contributor

    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 both instanceof C and instanceof Array would work.

  23. basarat commented on Jan 4, 2015

    @basarat
    Contributor

    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
        }
    }
  24. Arnavion commented on Jan 4, 2015

    @Arnavion
    Contributor

    Hmm, you're right. When I tested successfully with message: C | number[] and message instanceof Array, I did use an empty class for C. Adding a member to that class causes message instanceof Array to fail to narrow again.

    Maybe open a new issue.

  25. eggers commented on Oct 28, 2015

    @eggers

    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 number will satisfy the interface, and won't result in a compile error. In reality, most properties are optional, but either uri or url must 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
  26. mhegazy commented on Oct 28, 2015

    @mhegazy
    Contributor

    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.

  27. locked and limited conversation to collaborators on Jun 18, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusNeeds More InfoThe issue still hasn't been fully clarifiedSuggestionAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions