queq1890
queq1890

Reputation: 23

TypeScript: Get union type of nested object keys

I wanted to extract types of keys of nested objects and tried something like the below.

TS Playground link

type RecursiveRecord = {
  [key in string]: string | RecursiveRecord;
};

type Keys<T extends RecursiveRecord, K = keyof T> = K extends string
  ? T[K] extends string
    ? K
    : T[K] extends RecursiveRecord
    ? K | Keys<T[K]> // here I got error
    : never
  : never;

type Obj = {
  a: {
    c: 'aaaaaa';
    d: 'aaaaaaaaaaa';
    e: { f: 'q' };
  };
  b: 'dd';
};

export type A = Keys<Obj>; // want to get "a" | "b" | "c" | "d" | "e" | "f"

But on the K | Keys<T[K]>, I got the following type error. Is there any clean way to solve this?

index.ts:9:16 - error TS2344: Type 'T[K]' does not satisfy the constraint 'RecursiveRecord'.
  Type 'T[string]' is not assignable to type 'RecursiveRecord'.
    Type 'string | RecursiveRecord' is not assignable to type 'RecursiveRecord'.
      Type 'string' is not assignable to type 'RecursiveRecord'.

Upvotes: 2

Views: 1675

Answers (1)

jcalz
jcalz

Reputation: 327774

There's at least one open issue in TypeScript's GitHub repo about this problem: see microsoft/TypeScript#25804 for a suggestion to allow conditional types to keep track of constraints on more complicated checked types like T[K]. Right now that one is just listed as "awaiting more feedback" so if we want to see anything done about it we should probably give it an 👍 and describe our compelling use cases. I'm not sure if there's a more canonical GitHub issue for it, but for now, anyway, it's just the way the language is.

What we can do in situations where the compiler forgets some constraint we think it should remember is to "remind it". Usually my approach is: if the type XXX is supposed to be constrained by YYY but the compiler doesn't realize it, I replace XXX with Extract<XXX, YYY>, using the Extract utility type to "filter" XXX by YYY. If XXX is truly assignable to YYY then this filter will be a no-op, but now the compiler will recognize that Extract<XXX, YYY> is assignable to YYY.

So that gives you this:

type Keys<T extends RecursiveRecord, K = keyof T> =
  K extends string ? (
    T[K] extends string ? K :
    T[K] extends RecursiveRecord ? K | Keys<Extract<T[K], RecursiveRecord>> :
    never
  ) : never;

which resolves the error.


Of course for this operation I'd probably write something more like:

type NestedKeys<T> =
  T extends object ? { [K in keyof T]-?: K | NestedKeys<T[K]> }[keyof T] : never;

which, at least for your example, yields the same result:

type B = NestedKeys<Obj>
// type B = "a" | "b" | "c" | "d" | "e" | "f"

Playground link to code

Upvotes: 2

Related Questions