We'll start this of by creating a couple of interfaces, with a property type
that we can discriminate against:
interface Disciple {
type: 'disciple';
name: string;
}
interface Angel {
type: 'angel'
name: string;
wings: boolean;
}
interface Wolf {
type: 'animal';
name: string;
legs: number;
}
And we'll add them to a union like so:
type UnholyUnion = Disciple | Angel | Wolf;
So now to the problem; How can we create a new union from this, that's a subtype of our UnholyUnion
?
We would probably reach for Pick
to help us out here. It's documented as From T, pick a set of properties whose keys are in the union K
. Sounds about right.
type PickedFromUnion = Pick<UnholyUnion, 'type' | 'name'>
What we want:
type PickedFromUnion =
| {type: 'disciple'; name: string; }
| {type: 'angel'; name: string;}
| {type: 'animal'; name: string;}
What do we get? Well, not what we were hoping for..
type PickedFromUnion = {
type: 'disciple' | 'angel' | 'animal';
name: string
}
Why is that? Here's a discussion on GitHub where they explain that:
Pick<T, K>/Omit<T, K> are not distributive over union types
Ok, so how do we get what we want?
Here's one way:
type DistributedPick<Type, Keys extends keyof Type> = Type extends unknown
? Pick<Type, Keys>
: never
Running our type through this gives us:
type DistributedPickFromUnion = DistributedPick<UnholyUnion, 'type' | 'name'>
/**
| {type: 'disciple'; name: string; }
| {type: 'angel'; name: string;}
| {type: 'animal'; name: string;}
*/
How does this work?
To understand this, we first need to understand what unknown
is. Taken from the TS documentation on unknown:
unknown
is the type-safe counterpart ofany
. Anything is assignable tounknown
, butunknown
isn’t assignable to anything but itself andany
without a type assertion or a control flow based narrowing. Likewise, no operations are permitted on anunknown
without first asserting or narrowing to a more specific type.
We can test this with
type Test = Wolf extends unknown ? true : false;
// Test = true, because anything is assignable to unknown.
So, if the type we supply on Type
extends unkown
, (which we now know that all types do), we from Type
Pick
all supplied Keys
, ending up with a new union.
Mission accomplished!