Last active
March 27, 2025 19:53
-
-
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`.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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