You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Nested generic object of unions is not assignable to equivalent union of nested generic objectsΒ #51318
typeTest<T>={test: T}declareletouter: Test<Test<"a">>|Test<Test<"b">>declareletinner: Test<Test<"a"|"b">>inner=outer// this is acceptedouter=inner// this is not
π Actual behavior
The assignment outer = inner fails following error
Type 'Test<Test<"a" | "b">>' is not assignable to type 'Test<Test<"a">> | Test<Test<"b">>'.
Type 'Test<Test<"a" | "b">>' is not assignable to type 'Test<Test<"a">>'.
Type 'Test<"a" | "b">' is not assignable to type 'Test<"a">'.
Type '"a" | "b"' is not assignable to type '"a"'.
Type '"b"' is not assignable to type '"a"'.(2322)
π Expected behavior
As far as I can tell, it should be accepted, but the type checker isn't quite smart enough. This issue came up in a larger real world example, but this was the minimal reproduction I could think of.
In one case you have an object that can either store a or b, in the other case you either have an object that can only store a, or an object that can only store b. The types are not equivalent.
Although the assignment of outer to inner is already unsound.
Perhaps this is a more motivating example. I'm basically trying to use a custom result type that tracks possible errors, a bit like Haskell's Either or Rust's Result. However, when trying to use it in practice, the ergonomics isn't great, since TypeScript requires me to add quite a bit of type annotations to make the code compile.
interfaceSuccess<T>{ok: truevalue: T}interfaceFailure<E>{ok: falseerror: E}typeResult<T,E>=Success<T>|Failure<E>constsuccess=<T>(value: T): Success<T>=>({ok: true, value})constfailure=<E>(error: E): Failure<E>=>({ok: false, error})declarefunctiontest<T,E>(fn: (value: unknown)=>Result<T,E>): Result<T,E>// This works, for whatever reason.test((val)=>{if(typeofval!=="string")returnfailure("not a string"asconst)if(val.length===0)returnfailure("empty string"asconst)returnsuccess(val)})// This doesn't work. Why is that?test((val)=>{if(typeofval!=="string")returnfailure({code: "not a string"}asconst)if(val.length===0)returnfailure({code: "empty string"}asconst)returnsuccess(val)})// With an explicit type annotation, everything is ok.test((val): Result<string,{code: "not a string"|"empty string"}>=>{if(typeofval!=="string")returnfailure({code: "not a string"}asconst)if(val.length===0)returnfailure({code: "empty string"}asconst)returnsuccess(val)})
First, for types with a single value of their type parameter, indeed X<T | U> === X<T> | X<U>. TS is incomplete on this measure because it, in general, will instead rely on comparison using variance measurement when comparing two instantiations of the same generic type rather than structural comparison. However, using variance is absolutely critical for performance reasons. Figuring out what correctly constitutes a "single value" is also pretty tricky once you move beyond simple properties.
Second, in general, generic inference won't collect two disparate candidates and then infer to a union of them. This makes generics for the purposes of input validation largely useless, e.g. a call to something like find<T>(someNumberArray, someString) should not make a (sound!) inference of T = string | number, since reuse of a type parameter is intended to indicate at least some linkage of types. Callers intending that behavior can and should write find<string | number>(... in that case.
The middle test call is, from inference's perspective, an equivalent situation. There are two candidates to collect for E with no common type amongst them, but TS isn't willing to construct a union between them (nor would that union even work, given the lack of the single-use equivalence reduction rule noted above).
Activity
MartinJohns commentedon Oct 26, 2022
In one case you have an object that can either store a or b, in the other case you either have an object that can only store a, or an object that can only store b. The types are not equivalent.
Although the assignment of
outertoinneris already unsound.sluukkonen commentedon Oct 26, 2022
Perhaps this is a more motivating example. I'm basically trying to use a custom result type that tracks possible errors, a bit like Haskell's
Eitheror Rust'sResult. However, when trying to use it in practice, the ergonomics isn't great, since TypeScript requires me to add quite a bit of type annotations to make the code compile.RyanCavanaugh commentedon Oct 27, 2022
A few things to discuss.
First, for types with a single value of their type parameter, indeed
X<T | U> === X<T> | X<U>. TS is incomplete on this measure because it, in general, will instead rely on comparison using variance measurement when comparing two instantiations of the same generic type rather than structural comparison. However, using variance is absolutely critical for performance reasons. Figuring out what correctly constitutes a "single value" is also pretty tricky once you move beyond simple properties.Second, in general, generic inference won't collect two disparate candidates and then infer to a union of them. This makes generics for the purposes of input validation largely useless, e.g. a call to something like
find<T>(someNumberArray, someString)should not make a (sound!) inference ofT = string | number, since reuse of a type parameter is intended to indicate at least some linkage of types. Callers intending that behavior can and should writefind<string | number>(...in that case.The middle
testcall is, from inference's perspective, an equivalent situation. There are two candidates to collect forEwith no common type amongst them, but TS isn't willing to construct a union between them (nor would that union even work, given the lack of the single-use equivalence reduction rule noted above).github-actions commentedon Jun 8, 2023
This issue has been marked as 'Not a Defect' and has seen no recent activity. It has been automatically closed for house-keeping purposes.