typescripttypesnested

Dot notation key extraction for deeply nested objects, arrays and arrays of objects


I want to extract all deeply nested keys, including ones in arrays, but excluding intermediate index access:

type Pack = {
  name: string
  items: string[]
}

interface Params {
  name: string
  items: string[]
  pack: Pack
  packs: Pack[]
  nested1: {
    nested2: {
      nested3: string
    }[]
  }
}

type ParamKeys = <...> // expected: 'name' | 'items' | 'pack' | 'pack.name' | 'pack.items'| 'packs' | 'packs.name' | 'packs.items' | 'nested1' | 'nested1.nested2'| 'nested1.nested2.nested3'

I know this has been somewhat answered multiple times, but all solutions require array indexes, e.g. packs.8.name because the key is inferred as packs.${number}.name. In my case I want the user to construct a record of options that will apply to all array elements, therefore the index is not really useful and excludes nested elements in the array as required keys which is not what I want. Another less ideal solution would be to have the index be explicitly typed to zero if the above is not feasible, i.e. nested1.nested2.0.nested3 as a required key, for example


Solution

  • Algorithm:

    Implementation:

    First, types that we want to stop on, mostly primitives:

    type StopTypes = number | string | boolean | symbol | bigint | Date;
    

    Types to ignore:

    type ExcludedTypes = (...args: any[]) => any;
    

    Type that will allow us to create . notation. If the second argument is an empty string, returns the first one without a trailing .:

    type Dot<T extends string, U extends string> = '' extends U ? T : `${T}.${U}`;
    
    type GetKeys<T> = T extends StopTypes
        ? ''
        : T extends readonly unknown[]
        ? GetKeys<T[number]>
        : {
            [K in keyof T & string]: T[K] extends StopTypes
            ? K
            : T[K] extends ExcludedTypes
            ? never
            : K | Dot<K, GetKeys<T[K]>>;
        }[keyof T & string];
    

    We use indexed access to get the type of array type elements and mapped types to map through the keys of an object

    Testing:

    type Pack = {
        name: string;
        items: string[];
    };
    
    interface Params {
        name: string;
        items: string[];
        pack: Pack;
        packs: Pack[];
        fn: (...args: any) => any;
        nested1: {
            nested2: {
                nested3: string;
            }[];
        };
    }
    // "name" | "items" | "pack" | "packs" | "nested1" | "pack.name" | "pack.items" | "packs.name" | "packs.items" | "nested1.nested2" | "nested1.nested2.nested3"
    type ParamKeys = GetKeys<Params>;
    

    playground