Inference doesn't work with union types
zpdDG4gta8XKpMCd opened this issue · 9 comments
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 failureRelated question at StackOverflow: http://stackoverflow.com/questions/28931221/whats-the-use-of-union-types-in-typescript
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.
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 {}?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.
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 { /*...*/ }
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');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.
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.
Now fixed by #5738.
@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">.