2

Simplest example

assuming this type

type Foo = { a: number } | { b: string } | { c: boolean };

is it possible to get

type KeysOfFoo = 'a' | 'b' | 'c';

I tried this but it doesn't work

type Failed = keyof Foo; // never

TsPlayground

lonewarrior556
  • 3,917
  • 2
  • 26
  • 55

2 Answers2

5

Something like keyof (A | B | C) will result in only the keys that are definitely on an object of type A | B | C, meaning it would have to be a key known to be in all of A, B, and C, which is: keyof A & keyof B & keyof C. That is, keyof T is "contravariant in T". This isn't what you want though (in your case there are no keys in common so the intersection is never).

If you're looking for the set of keys which are in at least one of the members of your union, you need to distribute the keyof operator over the union members. Luckily there is a way to do this via distributive conditional types. It looks like this:

type AllKeys<T> = T extends any ? keyof T : never;

The T extends any doesn't do much in terms of type checking, but it does signal to the compiler that operations on T should happen for each union member of T separately and then the results would be united back together into a union. That means AllKeys<A | B | C> will be treated like AllKeys<A> | AllKeys<B> | AllKeys<C>. Let's try it:

type KeysOfFoo = AllKeys<Foo>;
// type KeysOfFoo = "a" | "b" | "c"

Looks good! Please note that you should be careful about actually using KeysOfFoo in concert with objects of type Foo. keyof is contravariant for a reason:

function hmm(foo: Foo, k: AllKeys<Foo>) {
  foo[k]; // error!
  // "a" | "b" | "c"' can't be used to index type 'Foo'.
  // Property 'a' does not exist on type 'Foo'
}

It's not safe to index into foo with k for the same reason you can't safely index into a value of type {a: number} with "b"... the key might not exist on the object. Obviously you know your use cases better than I do though, so you may well have some legitimate use of AllKeys<Foo> and Foo together. I'm just saying to be careful.


Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
0

Failed is of type never because your Foo type can't have any keys. It's currently set up as an intersection type between 3 completely exclusive types, so there are no valid keys.

If you change from using | to a &, then it'll work as is.

type Foo = { a: number } & { b: string } & { c: boolean }

type a = keyof Foo // 'a' | 'b' | 'c'

Playground Link

Zachary Haber
  • 10,376
  • 1
  • 17
  • 31
  • 2
    Note that `|` is a union, not an intersection. And the three types are not *exclusive*; they just don't share any declared properties. `{a: number}` and `{a: string}` would be exclusive because you can't have an object that satisfies both types. – jcalz May 08 '20 at 18:47