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
Indexed access behavior differs between type aliases and type parameters. #16036
Summary: Tagged<Opts>[keyof Opts] evaluates differently depending on whether Opts is a type alias or a type parameter.
Code
// Given an index type:typeMyOpts={x: number,y: string,};// pair each value with its index tag:typeTagged<T>={[KinkeyofT]: {tag: K,val: T[K]}};// which produces:// Tagged<MyOpts> == {// x: { tag: 'x', val: number },// y: { tag: 'y', val: string }// }// Then project the signature of that mapped type to get a tagged/disjoint union:typeMyTaggedUnion1=Tagged<MyOpts>[keyofMyOpts];// which produces:// MyTaggedUnion1 = { tag: 'x', val: number } | { tag: 'y', val: string }// which is exactly what we want.// But when trying to express Tagged<MyOpts>[keyof MyOpts] as a generic type expression,// the result turns out to be not as specific:typeTaggedUnion<Opts>=Tagged<Opts>[keyofOpts];typeMyTaggedUnion2=TaggedUnion<MyOpts>;// produces:// MyTaggedUnion2 = { tag: 'x' | 'y', val: number | string }
The evaluation of the literal expression Mapped<Tagged<MyOpts>> perhaps proceeds as follows. A literal is equivalent to an intersection of indexed types, one for each property:
typeMyOpts={x: number,y: string}
is the same (I presume?) as:
typeMyOpts={[kin'x']: number}&{[kin'y']: string};
The type expression Tagged<MyOpts> sees its argument as a product of indexed types, and the application of the mapped-type expression { [K in keyof T]: ⟦ E ⟧ } to that product is perhaps evaluated to a product of the mappings:
The next type expression, Merged<_>, also sees a product of indexed types, and the mapped-type expression that defines Merged is perhaps evaluated in the same way:
so when MergedTagged<_> is applied to { [k in 'x']: number } & { [k in 'y']: string }, the product of indexed expressions gets widened into a single indexed expression { [k in 'x'|'y']: string|number }:
If that's true, or even close to the mark, then I don't know if this still counts as a bug or not. But if plain type parameters <T> are secretly being constrained in ways that type literals are not (e.g., widened into a single indexed type?), then maybe it still counts as a bug?
This is due to one of our higher order mapped type relationships (#12351). Specifically, we have a rule that says a type { [P in K]: T}[X] is equivalent to an instantiation of T where X is substituted for every occurrence of P. An example of where we rely on this rule is:
functionsetState<T,KextendskeyofT>(obj: T,props: Pick<T,K>){for(letkinprops){obj[k]=props[k];// Pick<T, K>[K] is equivalent to T[K]}}
The reason you get different types from Tagged<MyOpts>[keyof MyOpts] versus TaggedUnion<MyOpts> is that the compiler doesn't "see" the relationship in the first construct because Tagged<MyOpts> and keyof MyOpts are eagerly instantiated. However, because TaggedUnion uses a type parameter, the instantiation is deferred and the equivalence becomes observable.
You can get the desired result by intersecting with a dummy index signature:
Activity
hearnden commentedon May 24, 2017
Define
Merged<T>of an indexed type as the union of its property types:such that a tagged union of T's properties can be expressed as
Merged<Tagged<T>>:evaluates as expected to:
{ tag: "x"; val: number; } | { tag: "y"; val: string; }But when
Merged<Tagged<_>>is expressed as a single operator:then:
evaluates unexpectedly to:
{ tag: "x" | "y"; val: number | string; }.hearnden commentedon May 24, 2017
I have a hunch as to why this is happening.
The evaluation of the literal expression
Mapped<Tagged<MyOpts>>perhaps proceeds as follows. A literal is equivalent to an intersection of indexed types, one for each property:is the same (I presume?) as:
The type expression
Tagged<MyOpts>sees its argument as a product of indexed types, and the application of the mapped-type expression{ [K in keyof T]: ⟦ E ⟧ }to that product is perhaps evaluated to a product of the mappings:The next type expression,
Merged<_>, also sees a product of indexed types, and the mapped-type expression that definesMergedis perhaps evaluated in the same way:But the body of a type operator's expression is perhaps evaluated differently.
If
Tis assumed to have a shape that's no more specific than a single indexed expression:{ [K in keyof T]: T[K] }, then:so when
MergedTagged<_>is applied to{ [k in 'x']: number } & { [k in 'y']: string }, the product of indexed expressions gets widened into a single indexed expression{ [k in 'x'|'y']: string|number }:If that's true, or even close to the mark, then I don't know if this still counts as a bug or not. But if plain type parameters
<T>are secretly being constrained in ways that type literals are not (e.g., widened into a single indexed type?), then maybe it still counts as a bug?RyanCavanaugh commentedon May 30, 2017
Anders Hejlsberg (@ahejlsberg) any idea what's going on here?
ahejlsberg commentedon May 30, 2017
This is due to one of our higher order mapped type relationships (#12351). Specifically, we have a rule that says a type
{ [P in K]: T}[X]is equivalent to an instantiation ofTwhereXis substituted for every occurrence ofP. An example of where we rely on this rule is:The reason you get different types from
Tagged<MyOpts>[keyof MyOpts]versusTaggedUnion<MyOpts>is that the compiler doesn't "see" the relationship in the first construct becauseTagged<MyOpts>andkeyof MyOptsare eagerly instantiated. However, becauseTaggedUnionuses a type parameter, the instantiation is deferred and the equivalence becomes observable.You can get the desired result by intersecting with a dummy index signature:
The index signature will never come into play, so its only effect is to disable the higher order equivalence pattern.
ahejlsberg commentedon Aug 25, 2017
Fixed by #18042.