Search code examples
typescripttypescript3.0

Is it possible to get the keys from a union of objects?


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


Solution

  • 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