Reputation: 23
I wanted to extract types of keys of nested objects and tried something like the below.
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
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"
Upvotes: 2