Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I remove a wider type from a union type without removing its subtypes in TypeScript?

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.

like image 665
Mihail Malostanidis Avatar asked Aug 21 '18 18:08

Mihail Malostanidis


2 Answers

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

like image 120
Mihail Malostanidis Avatar answered Oct 31 '22 01:10

Mihail Malostanidis


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
}
like image 1
nemo Avatar answered Oct 31 '22 01:10

nemo