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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

Spec Preview: Union types #805

Closed
RyanCavanaugh opened this Issue Oct 2, 2014 · 66 comments

Comments

Projects
None yet
@RyanCavanaugh
Member

RyanCavanaugh commented Oct 2, 2014

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". @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
}
@ivogabe

This comment has been minimized.

Show comment
Hide comment
@ivogabe

ivogabe Oct 2, 2014

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.

Contributor

ivogabe commented 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.

@johnnyreilly

This comment has been minimized.

Show comment
Hide comment
@johnnyreilly

johnnyreilly Oct 2, 2014

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)

johnnyreilly commented Oct 2, 2014

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Oct 2, 2014

Member

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;
Member

RyanCavanaugh commented Oct 2, 2014

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;
@johnnyreilly

This comment has been minimized.

Show comment
Hide comment
@johnnyreilly

johnnyreilly Oct 2, 2014

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.

johnnyreilly commented Oct 2, 2014

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.

@vvakame

This comment has been minimized.

Show comment
Hide comment
@vvakame

vvakame Oct 2, 2014

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;
}
Contributor

vvakame commented Oct 2, 2014

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;
}
@DanielRosenwasser

This comment has been minimized.

Show comment
Hide comment
@DanielRosenwasser

DanielRosenwasser Oct 2, 2014

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.

Member

DanielRosenwasser commented Oct 2, 2014

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.

@basarat

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Oct 2, 2014

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();
}
Contributor

basarat commented Oct 2, 2014

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();
}
@samuelneff

This comment has been minimized.

Show comment
Hide comment
@samuelneff

samuelneff commented Oct 2, 2014

馃憤

@ivogabe

This comment has been minimized.

Show comment
Hide comment
@ivogabe

ivogabe Oct 2, 2014

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;
Contributor

ivogabe commented Oct 2, 2014

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

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Oct 2, 2014

Member

[incorrect response/example removed]

Member

RyanCavanaugh commented Oct 2, 2014

[incorrect response/example removed]

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 2, 2014

Member

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

Member

ahejlsberg commented Oct 2, 2014

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

@danquirk

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Oct 2, 2014

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.

Member

danquirk commented Oct 2, 2014

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.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 2, 2014

Member

@DanielRosenwasser void is a subtype of any and no other types, so any|void becomes any but T|void never collapses to T otherwise.

Member

ahejlsberg commented Oct 2, 2014

@DanielRosenwasser void is a subtype of any and no other types, so any|void becomes any but T|void never collapses to T otherwise.

@ivogabe

This comment has been minimized.

Show comment
Hide comment
@ivogabe

ivogabe Oct 2, 2014

Contributor

馃憤

Contributor

ivogabe commented Oct 2, 2014

馃憤

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 2, 2014

Member

@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 {}[]. But with union types it succeeds because pair is an Array<Giraffe|Walrus> which is assignable to Array<Animal>. If we make it an error to infer a union type we'd be right back where we started (but for a different reason).

Member

ahejlsberg commented Oct 2, 2014

@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 {}[]. But with union types it succeeds because pair is an Array<Giraffe|Walrus> which is assignable to Array<Animal>. If we make it an error to infer a union type we'd be right back where we started (but for a different reason).

@danquirk

This comment has been minimized.

Show comment
Hide comment
@danquirk

danquirk Oct 3, 2014

Member

The argument is that that example is an error. If I wanted to allow makePair to be invoked with 2 arguments of different types then I would write the signature as:

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.

Member

danquirk commented Oct 3, 2014

The argument is that that example is an error. If I wanted to allow makePair to be invoked with 2 arguments of different types then I would write the signature as:

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.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 3, 2014

Member

If I wanted to allow makePair to be invoked with 2 arguments of different types...

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 ?: operator where each branch is a subtype of some common type and I'm forced to add a type assertion.

In our offline discussion about the map example yesterday I pointed out that the real problem is the co- and contra-variant parameter rule and that perhaps we should give less preference to inferences made in such positions. We should think about that some more.

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.

Member

ahejlsberg commented Oct 3, 2014

If I wanted to allow makePair to be invoked with 2 arguments of different types...

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 ?: operator where each branch is a subtype of some common type and I'm forced to add a type assertion.

In our offline discussion about the map example yesterday I pointed out that the real problem is the co- and contra-variant parameter rule and that perhaps we should give less preference to inferences made in such positions. We should think about that some more.

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.

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Oct 3, 2014

Member

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 x ? t : f than for [t, f]. Same for function return values and generic type inference - only one of those produces an error. Sometimes a failed BCT is an error, sometimes it isn't. As discussed earlier, it probably makes sense to continue enforcing that function return expressions produce a BCT that is in the candidate set, even in the presence of union types.

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.

$.each<T>(collection: T[], callback: (index: number, elem: T) => any);
$.map<T, U>(array: T[], callback: (elementOfArray: T, indexInArray: number) => U): U[];
ko.utils.arrayForEach<T>(array: T[], action: (item: T) => void): void;
ko.utils.arrayMap<T, U>(array: T[], mapping: (item: T) => U): U[];

We've discussed this family at length. Obviously, $.each([1, 2, 3], (n: number, s: string) => { }) should not be a valid call. Today, the {} type does not flow out of the function, so it's impossible to produce a type error here from a mismatched function argument type. #360, #810, or a possible rule specifically disallowing the contravariance of union-typed parameters would fix these. Using union types here doesn't improve the situation without some separate fix.

$.inArray<T>(value: T, array: T[], fromIndex?: number): number;
ko.utils.arrayIndexOf<T>(array: T[], item: T): number;

A call of the form $.inArray('hello', [1, 2, 3]) should be rejected; it's nonsense to allow this. This doesn't even require parameter contravariance. Allowing T to form a type union here doesn't make this any better. This also extends to functions like areSame<T>(lhs: T, rhs: T): boolean. Only #360 would make these an error.

$.grep<T>(array: T[], func: (elementOfArray: T, indexInArray: number) => boolean, invert?: boolean): T[];
ko.utils.arrayFilter<T>(array: T[], predicate: (item: T) => boolean): T[];
ko.utils.arrayFirst<T>(array: T[], predicate: (item: T) => boolean, predicateOwner?: any): T;

These functions at least return {} today when the inputs do not match. I don't think it would be an improvement for arrayFilter([1, 2, 3], isUppercaseString) to return Array<number|string> (?!) instead of Array<{}>. These would be completely fixed by #360 or #810.

$.merge<T>(first: T[], second: T[]): T[];

It's possible that you want merge([1, 2], ['a', 'b']) to return string|number, though in that case you could write merge with two type arguments rather than one, as Dan mentioned above.

ko.utils.compareArrays<T>(a: T[], b: T[]): Array<KnockoutArrayChange<T>>;

You don't want compareArrays([1, 2], ['a', 'b']) to succeed and produce a plausible-looking type any more than you would want 1 !== 'x' to succeed. Again, #360.

ko.utils.arrayPushAll<T>(array: T[], valuesToPush: T[]): T[];

This function mutates the underlying array, so forming a union type in the case of ko.utils.pushAll([1, 2, 3], ['x', 'y']) is simply the wrong thing to do. We would correctly reject this outright if it were a call like [1, 2].concat(["x"]);. Again, no function parameter contravariance is needed for this error to be missed. This is only fixed by #810, and is made worse by union types because the return type isn't immediately obviously wrong ({}[]).


The only function that I see as being improved by creating union types (merge) is one that could have its definition changed to explicitly produce Array<T|U> if that was the desired behavior. It's definitely a trade-off, but it seems like nearly every function in common use loses from this instead of gains.

Member

RyanCavanaugh commented Oct 3, 2014

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 x ? t : f than for [t, f]. Same for function return values and generic type inference - only one of those produces an error. Sometimes a failed BCT is an error, sometimes it isn't. As discussed earlier, it probably makes sense to continue enforcing that function return expressions produce a BCT that is in the candidate set, even in the presence of union types.

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.

$.each<T>(collection: T[], callback: (index: number, elem: T) => any);
$.map<T, U>(array: T[], callback: (elementOfArray: T, indexInArray: number) => U): U[];
ko.utils.arrayForEach<T>(array: T[], action: (item: T) => void): void;
ko.utils.arrayMap<T, U>(array: T[], mapping: (item: T) => U): U[];

We've discussed this family at length. Obviously, $.each([1, 2, 3], (n: number, s: string) => { }) should not be a valid call. Today, the {} type does not flow out of the function, so it's impossible to produce a type error here from a mismatched function argument type. #360, #810, or a possible rule specifically disallowing the contravariance of union-typed parameters would fix these. Using union types here doesn't improve the situation without some separate fix.

$.inArray<T>(value: T, array: T[], fromIndex?: number): number;
ko.utils.arrayIndexOf<T>(array: T[], item: T): number;

A call of the form $.inArray('hello', [1, 2, 3]) should be rejected; it's nonsense to allow this. This doesn't even require parameter contravariance. Allowing T to form a type union here doesn't make this any better. This also extends to functions like areSame<T>(lhs: T, rhs: T): boolean. Only #360 would make these an error.

$.grep<T>(array: T[], func: (elementOfArray: T, indexInArray: number) => boolean, invert?: boolean): T[];
ko.utils.arrayFilter<T>(array: T[], predicate: (item: T) => boolean): T[];
ko.utils.arrayFirst<T>(array: T[], predicate: (item: T) => boolean, predicateOwner?: any): T;

These functions at least return {} today when the inputs do not match. I don't think it would be an improvement for arrayFilter([1, 2, 3], isUppercaseString) to return Array<number|string> (?!) instead of Array<{}>. These would be completely fixed by #360 or #810.

$.merge<T>(first: T[], second: T[]): T[];

It's possible that you want merge([1, 2], ['a', 'b']) to return string|number, though in that case you could write merge with two type arguments rather than one, as Dan mentioned above.

ko.utils.compareArrays<T>(a: T[], b: T[]): Array<KnockoutArrayChange<T>>;

You don't want compareArrays([1, 2], ['a', 'b']) to succeed and produce a plausible-looking type any more than you would want 1 !== 'x' to succeed. Again, #360.

ko.utils.arrayPushAll<T>(array: T[], valuesToPush: T[]): T[];

This function mutates the underlying array, so forming a union type in the case of ko.utils.pushAll([1, 2, 3], ['x', 'y']) is simply the wrong thing to do. We would correctly reject this outright if it were a call like [1, 2].concat(["x"]);. Again, no function parameter contravariance is needed for this error to be missed. This is only fixed by #810, and is made worse by union types because the return type isn't immediately obviously wrong ({}[]).


The only function that I see as being improved by creating union types (merge) is one that could have its definition changed to explicitly produce Array<T|U> if that was the desired behavior. It's definitely a trade-off, but it seems like nearly every function in common use loses from this instead of gains.

@tomByrer

This comment has been minimized.

Show comment
Hide comment
@tomByrer

tomByrer Oct 3, 2014

Union types is a good idea.
I made a PR a few days ago where a perimeter previously only took a single number, I had expanded it to accept either a single number or a number array.

tomByrer commented Oct 3, 2014

Union types is a good idea.
I made a PR a few days ago where a perimeter previously only took a single number, I had expanded it to accept either a single number or a number array.

@RyanCavanaugh

This comment has been minimized.

Show comment
Hide comment
@RyanCavanaugh

RyanCavanaugh Oct 3, 2014

Member

Updates from 10/3 design meeting:

  • Disjoint properties are not present for the purposes of property access. {} was a bad idea here because anything is assignable to it. We may be able to use void instead, but will try just not having the properties at all at first to see how it goes
  • Due to implementation, number|string may appear as string|number if both types appear in a program. This should be nonconsequential
  • Merging of call signature sets occurs if their parameter lists are identical; return types of each signature get unioned together
  • The following BCT procedures will produce an error if the selected type is not in the candidate set:
    • Function return expressions
    • Generic type arguments
  • The remaining BCT procedures will union their types:
    • || operator
    • ? : operator
    • Array literal elements
    • Properties in an object literal contextually typed by a string indexer (probably unobservable, did not discuss)
Member

RyanCavanaugh commented Oct 3, 2014

Updates from 10/3 design meeting:

  • Disjoint properties are not present for the purposes of property access. {} was a bad idea here because anything is assignable to it. We may be able to use void instead, but will try just not having the properties at all at first to see how it goes
  • Due to implementation, number|string may appear as string|number if both types appear in a program. This should be nonconsequential
  • Merging of call signature sets occurs if their parameter lists are identical; return types of each signature get unioned together
  • The following BCT procedures will produce an error if the selected type is not in the candidate set:
    • Function return expressions
    • Generic type arguments
  • The remaining BCT procedures will union their types:
    • || operator
    • ? : operator
    • Array literal elements
    • Properties in an object literal contextually typed by a string indexer (probably unobservable, did not discuss)
@NoelAbrahams

This comment has been minimized.

Show comment
Hide comment
@NoelAbrahams

NoelAbrahams Oct 7, 2014

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 result instanceof Array.

NoelAbrahams commented Oct 7, 2014

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 result instanceof Array.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 7, 2014

Member

Latest commit on #824 has added support for type guards along the lines of "Local Meaning of Union Types" in the proposal. Some examples of what is now supported (copied from comment on commit):

function foo(x: number|string) {
    if (typeof x === "string") {
        return x.length;  // x has type string here
    }
    else {
        return x + 1;  // x has type number here
    }
}

function isLongString(obj: any) {
    return typeof obj === "string" && obj.length > 100;  // obj has type string after guard
}

function processData(data: string|() => string) {
    var d = typeof data !== "string" ? data() : data;  // d has type string
    // Process string in d
}

class NamedItem {
    name: string;
}

function getName(obj: any) {
    return obj instanceof NamedItem ? obj.name : "unknown";
}

The type of the local variable or parameter may be _narrowed_ in the following situations:

  • In an if statement with a condition that contains a type guard.
  • In a conditional statement with a condition that contains a type guard.
  • In the right operand of an && operation where the left operand contains a type guard.
  • In the right operand of an || operation where the left operand contains a type guard.

A _type guard_ is one of the following:

  • An expression of the form typeof x === "..." or typeof x !== "..." where x is the identifier of a variable or parameter and "..." is a string literal. When the string literal is "string", "number", or "boolean", the type guard either changes the type of x to that primitive type or removes that primitive type from the possible types of x (when x is of a union type). When the string literal is something other than a primitive type name, the type guard removes all primtive types from the possible types of x in cases where the guard is known to be true.
  • An expression of the form x instanceof C where x is the identifier of a variable or parameter and C is a constructor function. When the type guard is known to be true, the type of x is changed to the type of C.prototype.
  • An expression of the form !x where x is a type guard.
  • An expression of the form x && y where one or both of x and y are type guards.
  • An expression of the form x || y where one or both of x and y are type guards.

A type guard for a variable x has no effect if the statements or expressions it guards contain assignments to x. For example:

function foo(x: string|number) {
    if (typeof x === "string") {
        x = x.length;  // Fails, x still of type string|number here
    }
    var n = typeof x === "string" ? x.length : x;  // Ok, n has type number
}

Note that type guards affect types of simple variables and parameters only (e.g. x). They have no effect on the types of property accesses or other expressions (such as x.y and x()). The latter would require complicated aliasing analysis.

Also note that it is possible to defeat a type guard by calling a function that changes the type of the guarded variable. It would quite difficult to exhaustively prove that a variable isn't modified by function calls.

Member

ahejlsberg commented Oct 7, 2014

Latest commit on #824 has added support for type guards along the lines of "Local Meaning of Union Types" in the proposal. Some examples of what is now supported (copied from comment on commit):

function foo(x: number|string) {
    if (typeof x === "string") {
        return x.length;  // x has type string here
    }
    else {
        return x + 1;  // x has type number here
    }
}

function isLongString(obj: any) {
    return typeof obj === "string" && obj.length > 100;  // obj has type string after guard
}

function processData(data: string|() => string) {
    var d = typeof data !== "string" ? data() : data;  // d has type string
    // Process string in d
}

class NamedItem {
    name: string;
}

function getName(obj: any) {
    return obj instanceof NamedItem ? obj.name : "unknown";
}

The type of the local variable or parameter may be _narrowed_ in the following situations:

  • In an if statement with a condition that contains a type guard.
  • In a conditional statement with a condition that contains a type guard.
  • In the right operand of an && operation where the left operand contains a type guard.
  • In the right operand of an || operation where the left operand contains a type guard.

A _type guard_ is one of the following:

  • An expression of the form typeof x === "..." or typeof x !== "..." where x is the identifier of a variable or parameter and "..." is a string literal. When the string literal is "string", "number", or "boolean", the type guard either changes the type of x to that primitive type or removes that primitive type from the possible types of x (when x is of a union type). When the string literal is something other than a primitive type name, the type guard removes all primtive types from the possible types of x in cases where the guard is known to be true.
  • An expression of the form x instanceof C where x is the identifier of a variable or parameter and C is a constructor function. When the type guard is known to be true, the type of x is changed to the type of C.prototype.
  • An expression of the form !x where x is a type guard.
  • An expression of the form x && y where one or both of x and y are type guards.
  • An expression of the form x || y where one or both of x and y are type guards.

A type guard for a variable x has no effect if the statements or expressions it guards contain assignments to x. For example:

function foo(x: string|number) {
    if (typeof x === "string") {
        x = x.length;  // Fails, x still of type string|number here
    }
    var n = typeof x === "string" ? x.length : x;  // Ok, n has type number
}

Note that type guards affect types of simple variables and parameters only (e.g. x). They have no effect on the types of property accesses or other expressions (such as x.y and x()). The latter would require complicated aliasing analysis.

Also note that it is possible to defeat a type guard by calling a function that changes the type of the guarded variable. It would quite difficult to exhaustively prove that a variable isn't modified by function calls.

@mariusschulz

This comment has been minimized.

Show comment
Hide comment
@mariusschulz

mariusschulz Oct 8, 2014

Contributor

@ahejlsberg For my understanding: Shouldn't n have type number in your last code snippet?

Contributor

mariusschulz commented Oct 8, 2014

@ahejlsberg For my understanding: Shouldn't n have type number in your last code snippet?

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 8, 2014

Member

@mariusschulz Yes! Now fixed.

Member

ahejlsberg commented Oct 8, 2014

@mariusschulz Yes! Now fixed.

@basarat

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Oct 8, 2014

Contributor

what about (notice obj: number):

function isLongString(obj: number) {
    return typeof obj === "string" && obj.length > 100; // does obj have type string after guard?
}

just curious

Contributor

basarat commented Oct 8, 2014

what about (notice obj: number):

function isLongString(obj: number) {
    return typeof obj === "string" && obj.length > 100; // does obj have type string after guard?
}

just curious

@JsonFreeman

This comment has been minimized.

Show comment
Hide comment
@JsonFreeman

JsonFreeman Oct 8, 2014

Contributor

This looks great! Very clearly explained and well thought out!

I have one pedantic syntax question. In your processData example, the data parameter has the type annotation string | () => string. Is this / should this be syntactically legal? We had decided that => was to have lower precedence than |. So it would seem you have to write it as string | { (): string }

Contributor

JsonFreeman commented Oct 8, 2014

This looks great! Very clearly explained and well thought out!

I have one pedantic syntax question. In your processData example, the data parameter has the type annotation string | () => string. Is this / should this be syntactically legal? We had decided that => was to have lower precedence than |. So it would seem you have to write it as string | { (): string }

@johnnyreilly

This comment has been minimized.

Show comment
Hide comment
@johnnyreilly

johnnyreilly Oct 8, 2014

@basarat Interesting question - my hope would be that this was a compiler error. I can't think of a situation where that wouldn't be useful....

johnnyreilly commented Oct 8, 2014

@basarat Interesting question - my hope would be that this was a compiler error. I can't think of a situation where that wouldn't be useful....

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 8, 2014

Member

@basarat Type guards only affect variables of non-primitive types (i.e. variables whose dynamic type might be different from their declared type), so obj would still be of type number in your example.

Member

ahejlsberg commented Oct 8, 2014

@basarat Type guards only affect variables of non-primitive types (i.e. variables whose dynamic type might be different from their declared type), so obj would still be of type number in your example.

@ahejlsberg

This comment has been minimized.

Show comment
Hide comment
@ahejlsberg

ahejlsberg Oct 8, 2014

Member

New commit on #824 improves type argument inference for union types.

When inferring to a union type, we first infer to those types in the union that aren't naked type parameters. If that produces no new inferences, and if the union type contains a single naked type parameter, we then infer to that type parameter.

To infer from a union type we infer from each of the types in the union.

With these changes the following now works with zero type annotations in the actual code:

declare class Promise<T> {
    static resolve<T>(value: Promise<T>|T): Promise<T>;
    then<U>(onfulfilled?: (value: T) => Promise<U>|U): Promise<U>;
}

var p1 = Promise.resolve("hello");  // p1: Promise<string>
var p2 = Promise.resolve(p1);       // p2: Promise<string>
var p3 = p1.then(i => {             // p3: Promise<number>
    if (true) {
        return Promise.resolve(10);
    }
    else {
        return i.length;
    }
});
Member

ahejlsberg commented Oct 8, 2014

New commit on #824 improves type argument inference for union types.

When inferring to a union type, we first infer to those types in the union that aren't naked type parameters. If that produces no new inferences, and if the union type contains a single naked type parameter, we then infer to that type parameter.

To infer from a union type we infer from each of the types in the union.

With these changes the following now works with zero type annotations in the actual code:

declare class Promise<T> {
    static resolve<T>(value: Promise<T>|T): Promise<T>;
    then<U>(onfulfilled?: (value: T) => Promise<U>|U): Promise<U>;
}

var p1 = Promise.resolve("hello");  // p1: Promise<string>
var p2 = Promise.resolve(p1);       // p2: Promise<string>
var p3 = p1.then(i => {             // p3: Promise<number>
    if (true) {
        return Promise.resolve(10);
    }
    else {
        return i.length;
    }
});

@ahejlsberg ahejlsberg closed this Oct 8, 2014

@ahejlsberg ahejlsberg reopened this Oct 8, 2014

@luisrudge

This comment has been minimized.

Show comment
Hide comment
@luisrudge

luisrudge Oct 18, 2014

great example. i guess I was trying to compare with C#, but C# has method overloads so it isn't a fair comparison.

luisrudge commented Oct 18, 2014

great example. i guess I was trying to compare with C#, but C# has method overloads so it isn't a fair comparison.

@jedmao

This comment has been minimized.

Show comment
Hide comment
@jedmao

jedmao Oct 18, 2014

Contributor

@samuelneff if @nn's idea was implemented, your scenario would be better described as:

interface DatabaseSettings {
  connectionPool: false|ConnectionPoolSettings;
}
Contributor

jedmao commented Oct 18, 2014

@samuelneff if @nn's idea was implemented, your scenario would be better described as:

interface DatabaseSettings {
  connectionPool: false|ConnectionPoolSettings;
}
@saschanaz

This comment has been minimized.

Show comment
Hide comment
@saschanaz

saschanaz Oct 18, 2014

Contributor

I think we can just write connectionPool: ConnectionPoolSettings and utilize null.

Contributor

saschanaz commented Oct 18, 2014

I think we can just write connectionPool: ConnectionPoolSettings and utilize null.

@luisrudge

This comment has been minimized.

Show comment
Hide comment
@luisrudge

luisrudge commented Oct 19, 2014

@saschanaz agree 100% :)

@samuelneff

This comment has been minimized.

Show comment
Hide comment
@samuelneff

samuelneff Oct 19, 2014

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

samuelneff commented Oct 19, 2014

@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

This comment has been minimized.

Show comment
Hide comment
@saschanaz

saschanaz Oct 19, 2014

Contributor

@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);
  /* ... */
}
Contributor

saschanaz commented Oct 19, 2014

@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);
  /* ... */
}
@NoelAbrahams

This comment has been minimized.

Show comment
Hide comment
@NoelAbrahams

NoelAbrahams Oct 19, 2014

@ahejlsberg, @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

NoelAbrahams commented Oct 19, 2014

@ahejlsberg, @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
@NN---

This comment has been minimized.

Show comment
Hide comment
@NN---

NN--- Oct 19, 2014

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

NN--- commented Oct 19, 2014

@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

This comment has been minimized.

Show comment
Hide comment
@DouglasLivingstone

DouglasLivingstone Nov 15, 2014

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.

DouglasLivingstone commented Nov 15, 2014

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.

@NoelAbrahams

This comment has been minimized.

Show comment
Hide comment

NoelAbrahams commented Nov 16, 2014

@DouglasLivingstone

This comment has been minimized.

Show comment
Hide comment
@DouglasLivingstone

DouglasLivingstone commented Nov 17, 2014

@NoelAbrahams nice, thanks!

@jtheisen

This comment has been minimized.

Show comment
Hide comment
@jtheisen

jtheisen Nov 23, 2014

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

jtheisen commented Nov 23, 2014

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

@basarat

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Jan 2, 2015

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 
  }
}
Contributor

basarat commented Jan 2, 2015

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 
  }
}
@Arnavion

This comment has been minimized.

Show comment
Hide comment
@Arnavion

Arnavion Jan 2, 2015

Contributor

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

Contributor

Arnavion commented Jan 2, 2015

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

@basarat

This comment has been minimized.

Show comment
Hide comment
@basarat

basarat Jan 4, 2015

Contributor

@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
    }
}
Contributor

basarat commented Jan 4, 2015

@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
    }
}
@Arnavion

This comment has been minimized.

Show comment
Hide comment
@Arnavion

Arnavion Jan 4, 2015

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.

Contributor

Arnavion commented Jan 4, 2015

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.

@eggers

This comment has been minimized.

Show comment
Hide comment
@eggers

eggers Oct 28, 2015

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

eggers commented Oct 28, 2015

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
@mhegazy

This comment has been minimized.

Show comment
Hide comment
@mhegazy

mhegazy Oct 28, 2015

Contributor

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

Contributor

mhegazy commented Oct 28, 2015

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

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.