简体   繁体   中英

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]> 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'.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM