Search code examples
typescriptalgebraic-data-typestypescript-generics

Reason for [typof T] in generics having different semantics that a hard coded type?


So there is a trick I found in typescript to turn an object type into a discriminated union by mapping the type to key-value pair then creating a type that can be any value in the map using the keyof type. Here is a simple example:

type SourceType =
{
    foo: number,
    bar: string
};
type MapWithKey<T> = {[P in keyof T]: { key: P, value: T[P] }}
type DescriminatedUnion = MapWithKey<SourceType>[keyof SourceType];
//The DescriminatedUnion now has the following type
DescriminatedUnion ≡ {key:"foo",value:string} | {key:"bar",value:number}

This is very useful if you want to specify a very large discriminated union however when you try to make this construct fully generic you end up with a different type.

type MakeDescriminatedUnion<T> = MapWithKey<T>[keyof T];
type DescriminatedUnion = MakeDescriminatedUnion<SourceType>
//The DescriminatedUnion now has the followin type
DescriminatedUnion ≡ {key:"foo"|"bar",value:number|string}

This should be the same type but for some reason it isn't. I have tried to look through the typescript documentation to find some reasoning for this behavior however I cannot. Does anyone know the reasoning behind this difference? Or even better does anyone know a way to get around this behavior and make it fully generic?


Solution

  • Yes, this issue has bitten me and quite a few others. It's amazing that, as @artem mentions, a fix slated for TypeScript 2.6 was introduced just today!

    Meanwhile, for those of us stuck in TypeScript 2.4-land, there is a workaround using default generic type parameters:

    type MakeDiscriminatedUnion<T, M extends MapWithKey<T> = MapWithKey<T>> = M[keyof T];
    type DiscriminatedUnion = MakeDiscriminatedUnion<SourceType> // okay now
    

    The actual value of M doesn't get evaluated until you use MakeDiscriminatedUnion<SourceType>, so the compiler has no chance to "simplify" M[keyof T] the way it does above.

    Anyway, it's your choice whether to use the workaround or wait for TypeScript 2.6. Hope that helps. Good luck!