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