Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Inference doesn't work with union types #2264

Closed
zpdDG4gta8XKpMCd opened this issue Mar 8, 2015 · 9 comments
Closed

Inference doesn't work with union types #2264

zpdDG4gta8XKpMCd opened this issue Mar 8, 2015 · 9 comments
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue

Comments

@zpdDG4gta8XKpMCd
Copy link

I don't understand what is wrong with the following piece of code. It's a very basic scenario that I wish I could have since now union types are here. Please let me know what I am doing wrong:

interface Y { 'i am a very certain type': Y }
var y : Y = <Y> undefined;
function destructure<a, r>(
    something: a | Y,
    haveValue: (value: a) => r,
    haveY: (value: Y) => r
) : r {
    return something === y ? haveY(y) : haveValue(<a> something);
}

var value = Math.random() > 0.5 ? 'hey!' : <Y> undefined;

console.log(destructure(value, text => 'string', y => 'other one')); // <-- complete mess and failure

At playground: http://www.typescriptlang.org/Playground#src=interface%20Y%20%7B%20'i%20am%20a%20very%20certain%20type'%3A%20Y%20%7D%0Avar%20y%20%3A%20Y%20%3D%20%3CY%3E%20undefined%3B%0Afunction%20destructure%3Ca%2C%20r%3E(%0A%09something%3A%20a%20%7C%20Y%2C%0A%09haveValue%3A%20(value%3A%20a)%20%3D%3E%20r%2C%0A%09haveY%3A%20(value%3A%20Y)%20%3D%3E%20r%0A)%20%3A%20r%20%7B%0A%09return%20something%20%3D%3D%3D%20y%20%3F%20haveY(y)%20%3A%20haveValue(%3Ca%3E%20something)%3B%0A%7D%0A%0Avar%20value%20%3D%20Math.random()%20%3E%200.5%20%3F%20'hey!'%20%3A%20%3CY%3E%20undefined%3B%0A%0Aconsole.log(destructure(value%2C%20text%20%3D%3E%20'string'%2C%20b%20%3D%3E%20'other%20one'))%3B%20%2F%2F%20%3C--%20complete%20mess%20and%20failure%0A

Related question at StackOverflow: http://stackoverflow.com/questions/28931221/whats-the-use-of-union-types-in-typescript

@DanielRosenwasser
Copy link
Member

Maybe you could explain to me what exactly is a "complete mess and failure" here. I'm not seeing any errors getting reported in the Playground.

@zpdDG4gta8XKpMCd
Copy link
Author

short answer: text is of string | Y type (although the signature of destructure calls for string only)

slightly longer answer can be illustrated with this example:

interface A { 'i am A': A }
interface B { 'i am B': B }
interface C { 'i am C': C }
type X = A | B | C;
var x = Math.random() > 0.33 ? <A> undefined : (Math.random() > 0.5 ? <B> undefined : <C> undefined);
function destructure<a, b, c, r>(x: a | b | c, haveA: (value: a) => r, haveB: (value: b) => r, haveC: (value: c) => r) : r {
    /* here comes code smart enough to do destructuring */
    return undefined;
}

var who_am_i = destructure(x, a => 'a', b => 'b', c => 'c'); // <-- why is everything {}?

http://www.typescriptlang.org/Playground#src=interface%20A%20%7B%20'i%20am%20A'%3A%20A%20%7D%0Ainterface%20B%20%7B%20'i%20am%20B'%3A%20B%20%7D%0Ainterface%20C%20%7B%20'i%20am%20C'%3A%20C%20%7D%0Atype%20X%20%3D%20A%20%7C%20B%20%7C%20C%3B%0Avar%20x%20%3D%20Math.random()%20%3E%200.33%20%3F%20%3CA%3E%20undefined%20%3A%20(Math.random()%20%3E%200.5%20%3F%20%3CB%3E%20undefined%20%3A%20%3CC%3E%20undefined)%3B%0Afunction%20destructure%3Ca%2C%20b%2C%20c%2C%20r%3E(x%3A%20a%20%7C%20b%20%7C%20c%2C%20haveA%3A%20(value%3A%20a)%20%3D%3E%20r%2C%20haveB%3A%20(value%3A%20b)%20%3D%3E%20r%2C%20haveC%3A%20(value%3A%20c)%20%3D%3E%20r)%20%3A%20r%20%7B%0A%09%2F*%20here%20comes%20code%20smart%20enough%20to%20do%20destructuring%20*%2F%0A%09return%20undefined%3B%0A%7D%0A%0Avar%20who_am_i%20%3D%20destructure(x%2C%20a%20%3D%3E%20'a'%2C%20b%20%3D%3E%20'b'%2C%20c%20%3D%3E%20'c')%3B%20%2F%2F%20%3C--%20why%20is%20everything%20%7B%7D%3F

@zpdDG4gta8XKpMCd
Copy link
Author

I agree that in the last example there is no clue on how to match the formal and actual type parameters other than by the order of declaration. However in the first example it's sort of obvious.

@zpdDG4gta8XKpMCd
Copy link
Author

More to that, if functions of the following sort don't make any sense in TS (due to not being able to match formal and actual type parameters) why are they allowed?

function f<a, b, c, r>(value: a | b | c) : r { /*...*/ }

@DanielRosenwasser
Copy link
Member

if functions of the following sort don't make any sense in TS (due to not being able to match formal and actual type parameters) why are they allowed?

function f<a, b, c, r>(value: a | b | c) : r { /*...*/ }

See issue #360


You can overcome the other shortcomings you've encountered by simply adding a type annotation to each function you pass in.

interface A { 'i am A': A }
interface B { 'i am B': B }
interface C { 'i am C': C }
type X = A | B | C;
var x = Math.random() > 0.33 ? <A> undefined : (Math.random() > 0.5 ? <B> undefined : <C> undefined);
function dispatch<a, b, c, r>(x: a | b | c, haveA: (value: a) => r, haveB: (value: b) => r, haveC: (value: c) => r) : r {
    /* here comes code smart enough to dispatch */
    return undefined;
}

// Nothing is {}!
var who_am_i = dispatch(x,
                (a: A) => 'a',
                (b: B) => 'b',
                (c: C) => 'c');

@ahejlsberg
Copy link
Member

When inferring to a union type, type inference first attempts to infer to non-naked type parameters in the target. Failing that, if the union type contains a single naked type parameter, an inference is made to that type parameter. So, in your original example, we infer string | Y for a.

The type inference process never attempts to "carve up" a union type in discrete pieces. Because of structural typing it would be very hard to do so in a consistent and predictable manner. For example, what if value in your first example was of a type that is a union of string and a type derived from Y? I don't know of a meaningful rule we could implement to carve that up. (But proposals are certainly welcome!)

When inferring to a type that is a union of two or more naked type parameters, there are simply no reasonable or consistent inferences to make--so we make none. For example:

function foo<T, U>(x: T | U, f1: (value: T) => void, f2: (value: U) => void) { }

var v1: string | number;
var v2: number | string;
var v3: boolean | string | number;
foo(v1, x => {}, y => {});
foo(v2, x => {}, y => {});
foo(v3, x => {}, y => {});

There's just no meaningful way we could tease out consistent inferences for T and U in the above example.

@ahejlsberg ahejlsberg added the By Design Deprecated - use "Working as Intended" or "Design Limitation" instead label Mar 8, 2015
@zpdDG4gta8XKpMCd
Copy link
Author

a type that is a union of string and a type derived from Y? I don't know of a meaningful rule

can we say no in this situation? this case looks like a completely valid no-goer because out of all knowledge that we have we cannot conclude a right answer, but this case is in the minority of all other ones accounting for the rest 97.8% percent where the inference can be done no problem

I am not sure if that sounds like a proposal. I just wish problems were addressed not like all-or-nothing, but rather like we-do-what-we-can principle.

@ahejlsberg
Copy link
Member

Now fixed by #5738.

@RyanCavanaugh RyanCavanaugh added Fixed A PR has been merged for this issue and removed By Design Deprecated - use "Working as Intended" or "Design Limitation" instead labels Nov 24, 2015
@mhegazy mhegazy added the Bug A bug in TypeScript label Nov 24, 2015
@mhegazy mhegazy added this to the TypeScript 1.8 milestone Nov 24, 2015
@masaeedu
Copy link
Contributor

masaeedu commented Jun 24, 2017

@ahejlsberg Would it be possible to "carve up" a union type as you say, if all members of the union had a discriminant field? E.g. in the following example:

type Case<T extends string> = { _: T }
type Data = { _: "foo", x: string, y: number } | { _: "bar", z: Date }

function match<C1 extends string, U1 extends Case<C1>, C2 extends string, U2 extends Case<C2>>(
    data: U1 | U2,
    c1: C1, p1: (data: U1) => any,
    c2: C2, p2: (data: U2) => any) 
{ /* ... */ }

match(
    <Data>undefined,
    "foo", x => console.log('done'),
    "bar", x => console.log('done'))

It almost works, in that the two xs are inferred as Case<"foo"> and Case<"bar"> respectively, and I get a type error if the case labels don't match the data I am passing in. Unfortunately I want the inferred types for x to be the two members of the Data union, not just Case<"foo"> and Case<"bar">.

@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Bug A bug in TypeScript Fixed A PR has been merged for this issue
Projects
None yet
Development

No branches or pull requests

6 participants