I have a discriminated union type where a non-discriminant field appears on multiple members:
interface Foo {
type: 'foo';
foo: string;
}
interface OptionalFoo {
type: 'optionalfoo';
foo?: string;
}
interface RequiredNullableFoo {
type: 'requirednullable';
foo: string | null | undefined;
}
interface Bar {
type: 'bar';
bar: string;
}
type All = Foo | OptionalFoo | RequiredNullableFoo | Bar;
I would like to define a type that can create a new type from this:
type Foos = UnionMembersWithField<All, 'foo'>;
// Foos = Foo | OptionalFoo | RequiredNullableFoo;
I have a few promising attempts at defining UnionMembersWithField
, but none works completely:
// Utility type: `keyof All` is `'type'`, because it is the only field in common,
// but we want to accept any field from any union type.
type KeysOfUnion<T> = T extends T ? keyof T : never;
// This definition yields `Foo | RequiredNullableFoo` and drops `OptionalFoo`.
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = T extends Record<K, any> ? T : never;
// This definition does not compile due to TS1170, but seems like the best expression of intent.
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = T extends { [K]?: any } ? T : never;
// This definition yields `never`...
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = Required<T> extends Record<K, any> ? T : never;
// ...which is surprising, because redefining it with a helper `DiscriminateUnion` type yields
// `Required<Foo> | Required<OptionalFoo> | Required<RequiredNullableFoo>`
type DiscriminateUnion<T, K extends keyof T, V extends T[K]> = T extends Record<K, V> ? T : never;
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = DiscriminateUnion<Required<T>, K, any>;
The rest of my attempts variously did nothing (i.e. the output type was the same as the input type) or degenerated to never
.
Given that All['foo']
is any
(since Typescript doesn't give a good type if all union members don't have the specified field), I'm not even sure that such a type is possible.
CodePudding user response:
This should do the trick:
type UnionMembersWithField<U, F> =
U extends U // distribute U over the conditional
? F extends keyof U // check if the field F is in keyof U
? U
: never
: never
type Foos = UnionMembersWithField<All, 'foo'>;
// Foos = Foo | OptionalFoo | RequiredNullableFoo;
CodePudding user response:
You can use this:
type KeysOfUnion<T> = T extends T ? keyof T : never;
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = Exclude<T, Required<Exclude<T, Pick<Required<T>, K>>>>
Let's dive into it a bit. Let's start with this:
type UnionMembersWithField<T, K extends KeysOfUnion<T>> = Exclude<T,Exclude<T, Pick<T, K>>>
:
- We're picking the
key
out of every type - and then we're excluding those types from the union
- this will leave us with the complementary of what we want
- so we exclude once more
This will not take into account optional types, so we add Required
to end up with the final version.
Here a playground showcasing this.