Home > other >  Flattening type that gives a union of all keys in a nested object
Flattening type that gives a union of all keys in a nested object

Time:07-18

I feel like this should not be so difficult. Yet, no matter what I try, I can't get it to work. Here's the best I got so far:

// Sample type. I want to make this into 'a' | 'b' | 'x' | 'c' | 'd'
type O = Record<'a', Record<'b' | 'x', Record<'c' | 'd', string | number>>>;

// Explicit way of doing it, with limited depth
type Explicit = keyof O | keyof O[keyof O] | keyof O[keyof O][keyof O[keyof O]];

// What I hoped would work
type ExtractKeys<T> = T extends Record<infer U, any> ?
  keyof T | ExtractKeys<T[U]> :
  never;

// Or
type ExtractKeys2<T> = T extends Record<string, any> ?
  keyof T | ExtractKeys<T[keyof T]> :
  never;

// Try it
// TS2589: Type instantiation is excessively deep and possibly infinite.
const tryIt: ExtractKeys<O> = 'a';

// Can assign anything
const tryIt2: ExtractKeys2<O> = 'z';

The error is quite clear, I'm somehow ending up with infinite recursion. Yet I really do not see how? Nor do I find a better way. Any ideas?

Playground

CodePudding user response:

You had a typo in EtractKeys2. You used EtractKeys instead of ExtractKeys2 for your recursive call.

type ExtractKeys2<T> = T extends Record<string, any> 
  ? keyof T | ExtractKeys2<T[keyof T]> 
  : never

const tryIt2: ExtractKeys2<O> = 'z';

This works now.


Your first method failed because of the infer U. This check actually returns true for primitives.

type Test1 = number extends Record<string, any> ? true : false
//   ^? false

type Test2 = number extends Record<infer U, any> ? true : false
//   ^? true

Since this is always true, you end up with an infinite loop causing the "Type instantiation is excessively deep and possibly infinite" error.

Interestingly you end up with keyof Number with this inference.

type Test3 = number extends Record<infer U, any> ? U : false
//   ^? keyof Number

Playground

CodePudding user response:

As I understand it, you want to have a type that returns all the keys of the given object type and its object properties.

If this is the case, the following could be a possible approach:

export type Keys<T extends object> = {
    [K in keyof T]: T[K] extends object ? Keys<T[K]> | K : K
}[keyof T]

interface User {
    name: {
        first: string
        last: string
    }
}

const x: Keys<User> = 'first' // 'last', 'name'
type O = Record<'a', Record<'b' | 'x', Record<'c' | 'd', string | number>>>;

const o: Keys<O> = 'a' // 'b', 'c', 'd', 'x'

Explanation

Here, I map each key of T to the key type K itself. If the value of T[K], which is the key's value, should be an object, I create a union of the nested object's keys and the former key K itself.

[K in keyof T]: T[K] extends object ? Keys<T[K]> | K : K

Last, I generate a union of all those mentioned nested key unions using:

[keyof T]
  • Related