TypeScript Version: 2.1.4
Code
function or<A, B>(a: A, b: B) {
if (Math.random() < 0.5) return {value: a}
else return {value: b}
}
function readValue<T>({value}: {value: T}) {
return value
}
const numeric1 = or(1, '1')
const v = readValue(numeric1)
Expected behavior:
Compiled with no error.
Actual behavior:
Error on readValue call:
The type argument for type parameter 'T' cannot be inferred from the usage. Consider specifying the type arguments explicitly.
Workaround:
Explicitly add return type or<A, B>(): {value: A | B}.
This would be better asked as a Stack Overflow question.
That said, what you need is something like
function or<A, B>(a: A, b: B) {
if (Math.random() < 0.5) return { value: a };
else return { value: b };
}
function readValue<T extends { value: U | V }, U, V>(x: T) {
return x.value;
}
const numeric1 = or(1, '1');
const v = readValue(numeric1);
This is not a workaround.
I think this unification is hard to do in function call because union type is classified as secondary inference. That is, when an argument of type A | B is matched against a function typed <V>(v: V): void, only A is first considered. This is TypeScript typing feature. @aluanhaddad 's recipe correctly captures this, which forces TypeScript promote secondary inference to primary one.
The easiest way to avoid this is just eagerly access the field. numeric1.value will have number | string
In fact the proposed merge of types is incorrect.
When you have a {value: A} - it is an object that can hold only instances of A during its life. The same is for {value: B}. At the same time {value: A|B} means that the object will handle both A and B.
{value: A} | {value: B} means a choice between two objects capable of dealing with only one type of the value, in contrast {value: A|B} is a type of single object.
Though, the weak form of your relation could be true only for immutable objects:
{readonly value: A} | {readonly value: B} === {readonly value:A|B}
However since readonly does not impose immutability, it is still incorrect.
@HerringtonDarkholme Thanks for explaining why it works! That was very helpful to me. I knew it worked, but not why 鉂わ笍
@aluanhaddad With your sample code, v is inferred as type {}, but expected result should be number | string. So I don't think your solution is correct.
@HerringtonDarkholme Thank you for the explanation.
When an argument of type A | B is matched against a function typed
(v: V): void, only A is first considered.
Any reason behind such behavior? Performance?
@Artazor At least {value: A} | {value: B} is assignable to {value: A | B} which TypeScript already recognize.
Disclaimer: type inference in function is complex. I'm likely to mistaken implementation.
I guess this is the side effect of matching union type in inference. Consider this:
type Opt<T> = T | undefined
function get<T>(opt: Opt<T>): T {
if (!opt) throw new Error('empty value')
return opt
}
declare var opt: Opt<number>
get(opt) // number
By categorizing inferences as primary and secondary, we can first remove identical types in union, and only match type parameter against discriminated type. This improves inference quality, say, the above example gives you number. And when the improved inference fails, we can fall back to secondary inference.
After re-reading code I think TS just infers target type (parameter T in OP's example) from every constituent from union type (argument type in OP's example).
@mhegazy Could I ask why this issue is closed?
Most helpful comment
I think this unification is hard to do in function call because union type is classified as secondary inference. That is, when an argument of type
A | Bis matched against a function typed<V>(v: V): void, onlyAis first considered. This is TypeScript typing feature. @aluanhaddad 's recipe correctly captures this, which forces TypeScript promote secondary inference to primary one.The easiest way to avoid this is just eagerly access the field.
numeric1.valuewill havenumber | string