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
Algorithm:
T
is some type that we don't want to go deeper with return ''
T
is an array invoke the type for the elements of the T
T
, where K
is the key of T
:
T[K]
is some type that we don't want to go deeper with return K
T[K]
is some type that we want to ignore, return never
K
with a recursive check of the T[K]
, with prefix of ${K}.
(assuming that at this step we only work with objects)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>;