Typescript has a feature called keyof
that can be used to obtain the keys of an object. However, the keyof
operator only works for the first level of an object, and when we want to obtain all keys in a deep level, things get a bit more complicated. In this article, we will discuss how to implement a type that can obtain all keys in a deep level.
Overview of the Problem
To understand the problem we are trying to solve, let’s start with an example. Consider the following object:
const obj = {
a: {
b: 1,
c: {
d: 2,
e: 3
}
},
f: {
g: 4
}
}
If we want to obtain all the keys of this object, including the keys in the nested objects, we need a type that can recursively traverse the object and return all the keys. This can be a challenging task, especially for complex objects with multiple levels of nesting.
A Possible Solution
One approach to solving this problem is to use a recursive type definition. Typescript allows us to define recursive types using intersection types. An intersection type is a type that represents a value that has all the properties of all the types in the intersection.
We can define a recursive type that represents an object with a set of keys, where each key is either a primitive value or another object that also has a set of keys. Here’s how we can define this type:
type DeepKeys<T> = T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | `${K}.${DeepKeys<T[K]>}`
: never;
}[keyof T]
: never;
This type definition may seem a bit complex, so let’s break it down into smaller parts.
The type DeepKeys<T>
is a conditional type that checks if the input type T
is an object. If T
is an object, we use a mapped type to create a new object with the same keys as T
, but the values are the keys of the nested objects, represented as a string. If T
is not an object, we return an empty string.
The mapped type uses the keyof
operator to get the keys of the object, and then we use a conditional statement to check if each key is a string or number. If the key is a string or number, we concatenate it with a dot and the keys of the nested object, obtained recursively using DeepKeys<T[K]>
. If the key is not a string or number, we return never
, which means the key is not valid.
Using the Type
Now that we have defined the DeepKeys
type, we can use it to get the keys of any object with nested objects. Here's an example of how we can use it:
const obj = {
a: {
b: 1,
c: {
d: 2,
e: 3,
},
},
f: {
g: 4,
},
h: undefined,
};
type DeepKeys<T> = T extends object
? {
[K in keyof T]-?: K extends string | number
? `${K}` | `${K}.${DeepKeys<T[K]>}`
: never;
}[keyof T]
: never;
function getAllKeys<T extends object>(
obj: T,
prefix: string = '',
): DeepKeys<T>[] {
return Object.entries(obj).reduce((result: string[], [key, value]) => {
const newPrefix = prefix ? `${prefix}.${key}` : key;
return result.concat([
newPrefix,
...(typeof value === 'object' && value !== null
? getAllKeys(value, newPrefix)
: []),
]);
}, []) as DeepKeys<T>[];
}
const keys = getAllKeys(obj);
console.log(keys); // ["a" | "f" | "h" | "a.b" | "a.c" | "a.c.d" | "a.c.e" | "f.g"]
In this example, we define a function called getAllKeys
that takes an object as an argument and returns an array of all the keys in the object. We use the Object.keys
method to get the keys of the object, and then we cast the result to the DeepKeys
type to ensure that we get all the keys, including the keys in the nested objects.
Limitations
While the DeepKeys
type can be useful in many situations, it does have some limitations. One limitation is that it only works for objects with a finite depth. If we have an object with an infinite depth, such as an object that contains a reference to itself, the type definition will result in a stack overflow error.
Another limitation is that the resulting type can be very complex for objects with many levels of nesting, which can make it difficult to work with. In some cases, it may be necessary to use a simpler type definition or a different approach to get the keys of an object.
Conclusion
In this article, we have discussed how to implement a type that can obtain all the keys in an object, including the keys in the nested objects. We have used a recursive type definition to define the DeepKeys
type, which allows us to recursively traverse the object and return all the keys. We have also provided an example of how to use the DeepKeys
type to get the keys of an object.
While the DeepKeys
type has some limitations, it can be a useful tool for working with objects with nested objects.
If you found this helpful, please consider subscribing to my newsletter for more useful articles and tools about web development. Thanks for reading!