How can I remove a wider type from a union type without removing its subtypes in TypeScript?
Current solution (Typescript 4.1
+)
2021 Edit: The 2.8
implementation of KnownKeys<T>
is broken since Typescript 4.3.1-rc
, but a new, more semantic implementation using key remapping is available since 4.1
:
type RemoveIndex<T> = {
[ K in keyof T as string extends K ? never : number extends K ? never : K ] : T[K]
};
It can then be used as follows:
type KnownKeys<T> = keyof RemoveIndex<T>;
interface test {
req: string
opt?: string
[k: string]: any
}
type demo = KnownKeys<test>; // "req" | "opt" // Absolutely glorious!
Below is the preserved solution for pre-4.1
Typescript versions:
I got a solution from @ferdaber in this GitHub thread.
Edit: Turns out it was, to little fanfare, published in 1986 by @ajafff
The solution requires TypeScript 2.8's Conditional Types and goes as follows:
type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;
Below is my attempt at an explaination:
The solution is based on the fact that string
extends string
(just as 'a'
extends string
) but string
doesn't extend 'a'
, and similarly for numbers.
Basically, we must think of extends
as "goes into"
First it creates a mapped type, where for every key of T, the value is:
- if string extends key (key is string, not a subtype) => never
- if number extends key (key is number, not a subtype) => never
- else, the actual string key
Then, it does essentially valueof to get a union of all the values:
type ValuesOf<T> = T extends { [_ in keyof T]: infer U } ? U : never
Or, more exactly:
interface test {
req: string
opt?: string
[k: string]: any
}
type FirstHalf<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K
}
type ValuesOf<T> = T extends { [_ in keyof T]: infer U } ? U : never
// or equivalently, since T here, and T in FirstHalf have the same keys,
// we can use T from FirstHalf instead:
type SecondHalf<First, T> = First extends { [_ in keyof T]: infer U } ? U : never;
type a = FirstHalf<test>
//Output:
type a = {
[x: string]: never;
req: "req";
opt?: "opt" | undefined;
}
type a2 = ValuesOf<a> // "req" | "opt" // Success!
type a2b = SecondHalf<a, test> // "req" | "opt" // Success!
// Substituting, to create a single type definition, we get @ferdaber's solution:
type KnownKeys<T> = {
[K in keyof T]: string extends K ? never : number extends K ? never : K
} extends { [_ in keyof T]: infer U } ? U : never;
// type b = KnownKeys<test> // "req" | "opt" // Absolutely glorious!
Explaination in GitHub thread in case someone makes an objection over there
Per accepted answer: https://stackoverflow.com/a/51955852/714179. In TS 4.3.2 this works:
export type KnownKeys<T> = keyof {
[K in keyof T as string extends K ? never : number extends K ? never : K]: never
}