Using the Exclude operator doesn't work.
type test = Exclude<'a'|'b'|string, string>
// produces type test = never
I can understand why "except strings" also means excluding all the string literals, but how can I obtain 'a'|'b'
out of 'a'|'b'|string
?
If needed, assume latest TypeScript.
The usecase is as follows:
Say a third party library defines this type:
export interface JSONSchema4 {
id?: string
$ref?: string
$schema?: string
title?: string
description?: string
default?: JSONSchema4Type
multipleOf?: number
maximum?: number
exclusiveMaximum?: boolean
minimum?: number
exclusiveMinimum?: boolean
maxLength?: number
minLength?: number
pattern?: string
// to allow third party extensions
[k: string]: any
}
Now, what I want to do, is get a union of the KNOWN properties:
type KnownProperties = Exclude<keyof JSONSchema4, string|number>
Somewhat understandably, this fails and gives an empty type.
If you are reading this but I was hit by a bus, the answer to this might be found in this GitHub thread.
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!
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:
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
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With