Search code examples
typescriptrecursion

Advanced typescript: find all the values of the key "id" in a nested object


I'm trying to extract all the values of the keys "id" in the object, at the first level and at all the nested levels.

Given my ExampleType, I want NestedID<ExampleType> to be "one" | "two" | "three" | "four". However, I'm doing something wrong and it only gives me "one", and I don't understand why.

Does somebody understands why this is not working ?

I got the inspiration from https://dev.to/pffigueiredo/typescript-utility-keyof-nested-object-2pa3 which is a really good tutorial.

type ExampleType = {
  id: "one"
  children: [{ id: "two" }, { id: "three"; children: [{ id: "four" }] }, { abc: "def" }]
  efg: "hij"
}

type NestedId<ObjectType extends Record<string, any> | undefined> = {
  [Key in keyof ObjectType & string]: ObjectType[Key] extends undefined
    ? never
    : Key extends "id" // if the key is "id"
      ? ObjectType[Key] // return the value of the key
      : Key extends "children" // else, if the key is "children"
        ? ObjectType[Key] extends Array<Record<string, any> | undefined> // and its value is an array of object
          ? NestedId<ObjectType[Key][number]> // recursive call
          : never // else, children value is not an object --> return never
        : never // else, key is not "id" or "children" --> return never
}[keyof ObjectType & string]

const id1: NestedId<ExampleType> = "one" // works as expected
const id2: NestedId<ExampleType> = "two" // doesn't work (Type "two" is not assignable to type "one")
const id3: NestedId<ExampleType> = "three" // doesn't work (Type "three" is not assignable to type "one")
const id4: NestedId<ExampleType> = "four" // doesn't work (Type "four" is not assignable to type "one")
const foo: NestedId<ExampleType> = "foo" // works as expected
const bar: NestedId<ExampleType> = "bar" // works as expected

Solution

  • You can use infer to achieve this. It gets the parent id and recursively checks the children props to add the ids in from there if they exist.

    type NestedId<ObjectType> = 
      ObjectType extends { id: infer IdType } 
        ? IdType | (ObjectType extends { children: Array<infer ChildType> } 
          ? NestedId<ChildType> 
          : never) 
        : never;
    

    In this example you get "one" | "two" | "three" | "four" back, and everything else like "foo" will error.