Typescript Trick: Retrieving All Keys of an Object

Discover how to implement a type in Typescript that can obtain all keys in a deep level of a nested object.

Zachary Lee
Level Up Coding

--

Photo by Andy Carne on Unsplash

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.

Thanks for reading. If you like such stories and want to support me, please consider becoming a Medium member. It costs $5 per month and gives unlimited access to Medium content. I’ll get a little commission if you sign up via my link.

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Join the Level Up talent collective and find an amazing job

--

--