Home > Net >  typescript: filter union type to members with a field present
typescript: filter union type to members with a field present

Time:09-13

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;

Playground

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.

  • Related