Reputation: 486
As a followup to this answer, I'm trying to write a generic type that maps a tag to a type that's part of a discriminated union.
The generic version given in the above answer works:
type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = T extends Record<K, V> ? T : never
But I can't make my own version that's not generic over T
(the union itself). It does work if I make the union type generic with a default, which I find weird.
Here's my code:
interface TypeA {
tag: "a";
data: string;
}
interface TypeB {
tag: "b";
data: [number];
}
interface TypeCD {
tag: "c" | "d";
data: number;
}
type Union = TypeA | TypeB | TypeCD;
type DiscriminatedUnion_0<V extends Union["tag"]> = Union extends Record<"tag", V> ? Union : never;
let shouldBeTypeA_0: DiscriminatedUnion_0<"a">; // doesn't work, type 'never'
// this works
type DiscriminatedUnion_1<V extends Union["tag"], T extends Union = Union> = T extends Record<"tag", V> ? T : never;
let shouldBeTypeA_1: DiscriminatedUnion_1<"a">;
type DiscriminatedUnion_2<V extends Union["tag"], T extends Union> = T extends Record<"tag", V> ? T : never;
let shouldBeTypeA_2: DiscriminatedUnion_2<"a", Union>;
Upvotes: 3
Views: 143
Reputation: 328503
The technique in DiscriminateUnion
uses a distributive conditional type which only works if you are checking a bare generic type parameter. That link explains it pretty well, or you can read a longer explanation if you want. The only way to get this behavior is to do the conditional on a bare generic type parameter somewhere. That's why DiscriminatedUnion_1
worked; you checked the type parameter T
.
Luckily you don't have to play games with default type parameters to get this effect. The generic type parameter check has to happen, but it doesn't have to be in directly in your final type alias.
One way to do it is to use the original DiscriminateUnion<T, K, V>
definition and make an alias that uses it, like
type MyDiscriminateUnion<V extends Union["tag"]> = DiscriminateUnion<Union, "tag", V>;
Another way to do this, though, is to use a predefined type alias in the standard library called Extract<T, U>
, which returns all constituents of a union type T
that match another type U
. The definition is as follows:
type Extract<T, U> = T extends U ? T : never;
With that, you can construct your DiscriminatedUnion
without playing games with default type parameters:
type DiscriminatedUnion<V extends Union["tag"]> = Extract<Union, Record<"tag", V>>;
let shouldBeTypeA: DiscriminatedUnion<"a">; // TypeA, hooray!
Okay, hope that helps; good luck!
Upvotes: 3