35

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.

jcalz
  • 264,269
  • 27
  • 359
  • 360
Mihail Malostanidis
  • 2,686
  • 3
  • 22
  • 38
  • `'a'|'b'|string` simplifies to `string` before you can do anything else with it. You'll have to change whatever code is generating this type. What is that code? – Matt McCutchen Aug 21 '18 at 18:27
  • 2
    @MattMcCutchen that union is the result of `keyof` on an interface. I am editing the question right now. – Mihail Malostanidis Aug 21 '18 at 18:28
  • The crux of the issue is to remove the index signature. In a few minutes of messing around, I was unable to find a way to do it. In [this question](https://stackoverflow.com/questions/51465182/typescript-remove-index-signature-using-mapped-types), jcalz claims it can't be done, and I'd tend to believe him. – Matt McCutchen Aug 21 '18 at 18:43
  • 2
    @MattMcCutchen I found a way, I added it as an answer. – Mihail Malostanidis Aug 21 '18 at 19:59
  • That is amazing... someone go to the other thread and answer it or link it here. – jcalz Aug 21 '18 at 20:10
  • @jcalz I answered it there, I can't accept my answer here and hence do a duplicate link (Is this even a duplicate?) for two more days though. - https://stackoverflow.com/a/51956054/2115619 – Mihail Malostanidis Aug 21 '18 at 20:13

2 Answers2

43

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

Mihail Malostanidis
  • 2,686
  • 3
  • 22
  • 38
  • 8
    Wow! That is very clever. – jcalz Aug 21 '18 at 20:08
  • 2
    The answer is brilliant!! Bout I think the interface test.opt should be optional, since type a is generated from FirstHalf and a.opt has value `undefined` – ZiZi Zheng Jan 13 '21 at 04:43
  • Did not work here: ```const keysToSync = ['apitoken', 'userId', 'active-trade-account'] as const; type KnownKeys = { [K in keyof T]: string extends K ? never : number extends K ? never : K } extends { [_ in keyof T]: infer U } ? U : never; type KeysToSync = KnownKeys; ``` – Devin Rhode Mar 14 '21 at 23:26
  • @DevinRhode `KnownKeys` works on keyed collections, not naked unions. `KnownKeys` correctly produces `"0" | "1" | "2"`. (Yes, it is unfortunate they are strings and not numbers, but that's the truth of the indexing semantics of JS). Notably, the `KnownKeys` *is* useful here in that it removes the `number | ` from the type. – Mihail Malostanidis Mar 15 '21 at 13:23
1

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
}
nemo
  • 12,241
  • 3
  • 21
  • 26