Skip to content

Instantly share code, notes, and snippets.

@ryangoree
Last active March 27, 2025 19:53
Show Gist options
  • Save ryangoree/5afef42e5eeb33d45e9625ce56dabc9d to your computer and use it in GitHub Desktop.
Save ryangoree/5afef42e5eeb33d45e9625ce56dabc9d to your computer and use it in GitHub Desktop.
Extracts and transforms members of a union type `T` based on a filter type `F`.
/**
* Extracts and transforms members of a union type `T` based on a filter type
* `F`. It's similar to `Extract<T, U>` but rather than omitting members of `T`
* that aren't wholly assignable to `U`, (e.g., omitting `{ a: string }` when
* `U` is `{ a: 'foo' }`), it narrows values of `T` that the filter type `F` is
* assignable to (e.g., `{ a: string }` is narrowed to `{ a: 'foo' }`).
*
* For each member in `T` (distributing over unions), if it includes all
* required keys (as determined by {@linkcode RequiredValueKey<F>}), the type is
* transformed by applying the filter:
* - For each key `K` in `T` that exists in `F`, if `T[K]` is assignable to
* `F[K]`, the original type is kept.
* - Otherwise, the type from `F[K]` is substituted in.
*
* **Important:** If the resulting transformed type is not assignable to the
* original member of `T`, that union member is omitted from the output.
*
* @typeParam T - The union type whose members are to be filtered and
* transformed.
* @typeParam F - The filter type specifying required keys and their desired
* types.
*
* @example
* ```ts
* type Filtered = ExtractFiltered<
* { i: 0; a: string; c: boolean } | { i: 1; b: number } | { i: 2; c: boolean },
* { i: number; c: true }
* >;
* // -> { i: 0; a: string; c: true } | { i: 2; c: true };
*
* type FilteredEmpty = ExtractFiltered<{ a: string; b: number } | { a: number }, {}>;
* // -> { a: string; b: number } | { a: number } (No filtering if F is {})
*
* type FilteredPartial = ExtractFiltered<{ a: string; b: number; c: boolean }, { a: 'foo' }>;
* // -> { a: 'foo'; b: number; c: boolean }
*
* ```
*/
export type ExtractFiltered<T, F = {}> = T extends T // <- Distribute union
? RequiredValueKey<F> extends keyof T
? ApplyFilter<T, F> // <- Apply filter to entry
: never // <- Omit if entry is missing a required value
: never;
type ApplyFilter<T, F> = {
[K in keyof T]: K extends keyof F
? T[K] extends F[K]
? T[K] // <- Leave value as-is if it fits the filter
: F[K] // <- Use filter value otherwise
: T[K]; // <- Leave value as-is if the key isn't in the filter
} extends infer TF extends T
? TF // <- Return the transformed type if it's assignable to T
: never;
/**
* Get a union of all keys in {@linkcode T} that are required and not assignable
* to undefined.
*/
export type RequiredValueKey<T> = {
[K in keyof T]-?: undefined extends T[K] ? never : K;
}[keyof T];
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment