'Exhaustive list of keys of type

I have some type Foo and I would like to extract the keys of Foo. For now I can do it manually like so.

type Foo = {
  a?: string;
  b?: string;
};

const fooKeys = ["a", "b"] as Readonly<Array<keyof Foo>>;

But then somebody could come along and add c and fooKeys is still valid despite it not containing all the keys. How can I enforce that fooKeys must be exhaustive?

type Foo = {
    a?: string;
    b?: string;
    c: string;
  };

const fooKeys = ["a", "b"] as Readonly<Array<keyof Foo>>;

My current workaround is to define an example and infer everything from that, which is okay even though the types don't line up perfectly:

const exampleFoo = {
  a: 'example' as string | undefined,
  b: 'example' as string | undefined,
  c: 'example' as string
}

type Foo = typeof exampleFoo
const fooKeys = Object.keys(exampleFoo) as Readonly<Array<keyof Foo>>;

Maybe there is a better approach?



Solution 1:[1]

I think using an example object is ok.

This would be another option:

type Foo = {
    a?: string;
    b?: string;
    c: string;
};

const Fields: Record<keyof Foo, 1> = { a: 1, b: 1, c: 1}
const fooKeys = Object.keys(Fields) as Readonly<Array<keyof Foo>>;

Playground Link

There are solutions to convert keyof Foo to a tuple type, but they tend to be unreliable as order in a union is not guaranteed to always be the same or if you generate a union with all possible combinations that will kill compiler performance.

You could also use a function to validate all keys have been specified:

type HasAllUnique<Tuple, Union, Seen = never> = 
  Tuple extends [infer Head, ...infer Tail] 
    ? Head extends Seen 
      ? { "? ERROR ?": ["The following key appears more than once in the tuple:", Head]}
      : HasAllUnique<Tail, Exclude<Union, Head>, Head | Seen>
    : [Union] extends [never]? unknown : { "? ERROR ?": ["Some keys are missing from the tuple:", Union]}


function allKeys<T>() {
  return function<K extends Array<keyof T>>(...k : K & HasAllUnique<K, keyof T>) {
    return k
  }
}

allKeys<Foo>()("a") // errro
allKeys<Foo>()("a", "b") // error
allKeys<Foo>()("a", "b", "c") // ok
allKeys<Foo>()("a", "b", "c", "a") // error

Playground Link

Playground Link with comments

You could also have a functionless approach if you are ok with defining an extra type after the constant:


const bad2= ["a", "b"] as const
type E2 = Validate<HasAllUnique<typeof bad2, keyof Foo>>

const ok = ["a", "b", "c"] as const
type E3 = Validate<HasAllUnique<typeof ok, keyof Foo>>

Playground Link

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1