DEV Community

Cover image for How to Create a Type to Retrieve All Keys of an Object in TypeScript
Zachary Lee
Zachary Lee

Posted on • Originally published at webdeveloper.beehiiv.com on

How to Create a Type to Retrieve All Keys of an Object in TypeScript

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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"]
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (2)

Collapse
 
jonrandy profile image
Jon Randy πŸŽ–οΈ

Does this account for Symbol keys in objects?