Suppose there is a union type Thing
grouping together types Foo
, Bar
and Baz
with the discriminating property
In TypeScript v2.7 and earlier, there is no programmatic way to do this. It is easier to have TypeScript build unions programmatically than it is to inspect them. Therefore, you could do this instead:
interface UnionSchema {
Foo: {foo: string},
Bar: {bar: number},
Baz: {baz: boolean}
}
type Union<K extends keyof UnionSchema = keyof UnionSchema> = {
[P in K]: UnionSchema[K] & {tag: K}
}[K]
Now you can use Union
as you did before, but the individual union constituents can be referred to as Union<'Foo'>
, Union<'Bar'>
, and Union<'Baz'>
. For convenience you can still give them names:
interface Foo extends Union<'Foo'> {}
interface Bar extends Union<'Bar'> {}
interface Baz extends Union<'Baz'> {}
And type your function like this:
type TypeToFunc<U extends Union> = {
readonly [T in U['tag']]: (x: Union<T>) => string
}
const typeToFunc: TypeToFunc<Union> = {
// x must be of type Foo
Foo: x => `FOO: ${x.foo}`,
// x must be of type Bar
Bar: x => `BAR: ${x.bar}`,
// x must be of type Baz
Baz: x => `BAZ: ${x.baz}`,
}
Starting in TypeScript v2.8, there will be a feature called conditional types which allows a lot more expressivity in the type system. You can write a general union discriminator like this:
type DiscriminateUnion<T, K extends keyof T, V extends T[K]> =
T extends Record<K, V> ? T : never
And then, with your original definitions:
interface Foo {
tag: 'Foo'
foo: string
}
interface Bar {
tag: 'Bar'
bar: number
}
interface Baz {
tag: 'Baz'
baz: boolean
}
type Union = Foo | Bar | Baz
You get the almost magical:
type TypeToFunc<U extends Union> = {
readonly [T in U['tag']]: (x: DiscriminateUnion<Union,'tag',T>) => string
}
which also works. You can try this out now if you install typescript@next
from npm
... otherwise you'll need to wait.
Hope that helps; good luck!