Skip to content

TypeScript Tutorial: Extracting All Keys from Nested Object

Published: at 

What’s the Problem?

Let’s say you have a big TypeScript object. It has objects inside objects. You want to get all the keys, even the nested ones. But TypeScript doesn’t provide this functionality out of the box.

Look at this User object:

type 
type User = {
    id: string;
    name: string;
    address: {
        street: string;
        city: string;
    };
}
User
= {
id: stringid: string; name: stringname: string;
address: {
    street: string;
    city: string;
}
address
: {
street: stringstreet: string; city: stringcity: string; }; };

You want “id”, “name”, and “address.street”. The standard approach is insufficient:

// little helper to prettify the type on hover
type type Pretty<T> = { [K in keyof T]: T[K]; }Pretty<function (type parameter) T in type Pretty<T>T> = {
  [function (type parameter) KK in keyof function (type parameter) T in type Pretty<T>T]: function (type parameter) T in type Pretty<T>T[function (type parameter) KK];
} & {};

type type UserKeys = keyof UserUserKeys = keyof 
type User = {
    id: string;
    name: string;
    address: {
        street: string;
        city: string;
    };
}
User
;
type
type PrettyUserKeys = "id" | "name" | "address"
PrettyUserKeys
= type Pretty<T> = { [K in keyof T]: T[K]; }Pretty<type UserKeys = keyof UserUserKeys>;

This approach returns the top-level keys, missing nested properties like “address.street”.

We need a more sophisticated solution using TypeScript’s advanced features:

  1. Conditional Types (if-then for types)
  2. Mapped Types (change each part of a type)
  3. Template Literal Types (make new string types)
  4. Recursive Types (types that refer to themselves)

Here’s our solution:

type type ExtractKeys<T> = T extends object ? { [K in keyof T & string]: K | (T[K] extends object ? `${K}.${ExtractKeys<T[K]>}` : K); }[keyof T & string] : neverExtractKeys<function (type parameter) T in type ExtractKeys<T>T> = function (type parameter) T in type ExtractKeys<T>T extends object
  ? {
      [function (type parameter) KK in keyof function (type parameter) T in type ExtractKeys<T>T & string]: 
        | function (type parameter) KK 
        | (function (type parameter) T in type ExtractKeys<T>T[function (type parameter) KK] extends object ? `${function (type parameter) KK}.${type ExtractKeys<T> = T extends object ? { [K in keyof T & string]: K | (T[K] extends object ? `${K}.${ExtractKeys<T[K]>}` : K); }[keyof T & string] : neverExtractKeys<function (type parameter) T in type ExtractKeys<T>T[function (type parameter) KK]>}` : function (type parameter) KK);
    }[keyof function (type parameter) T in type ExtractKeys<T>T & string]
  : never;

Let’s break down this type definition:

  1. We check if T is an object.
  2. For each key in the object:
  3. We either preserve the key as-is, or
  4. If the key’s value is another object, we combine the key with its nested keys
  5. We apply this to the entire type structure

Now let’s use it:


type 
type UserKeys = "id" | "name" | "address" | "address.street" | "address.city"
UserKeys
= type ExtractKeys<T> = T extends object ? { [K in keyof T & string]: K | (T[K] extends object ? `${K}.${ExtractKeys<T[K]>}` : K); }[keyof T & string] : neverExtractKeys<
type User = {
    id: string;
    name: string;
    address: {
        street: string;
        city: string;
    };
}
User
>;

This gives us all keys, including nested ones.

The practical benefits become clear in this example:

const const user: Useruser: 
type User = {
    id: string;
    name: string;
    address: {
        street: string;
        city: string;
    };
}
User
= {
id: stringid: "123", name: stringname: "John Doe",
address: {
    street: string;
    city: string;
}
address
: {
street: stringstreet: "Main St", city: stringcity: "Berlin", }, }; function function getProperty(obj: User, key: UserKeys): anygetProperty(obj: Userobj:
type User = {
    id: string;
    name: string;
    address: {
        street: string;
        city: string;
    };
}
User
, key: "id" | "name" | "address" | "address.street" | "address.city"key: type UserKeys = "id" | "name" | "address" | "address.street" | "address.city"UserKeys) {
const const keys: string[]keys = key: "id" | "name" | "address" | "address.street" | "address.city"key.String.split(separator: string | RegExp, limit?: number): string[] (+1 overload)
Split a string into substrings using the specified separator and return them as an array.
@paramseparator A string that identifies character or characters to use in separating the string. If omitted, a single-element array containing the entire string is returned.@paramlimit A value used to limit the number of elements returned in the array.
split
(".");
let let result: anyresult: any = obj: Userobj; for (const const k: stringk of const keys: string[]keys) { let result: anyresult = let result: anyresult[const k: stringk]; } return let result: anyresult; } // This works function getProperty(obj: User, key: UserKeys): anygetProperty(const user: Useruser, "address.street"); // This gives an error function getProperty(obj: User, key: UserKeys): anygetProperty(const user: Useruser, "address.country");
Argument of type '"address.country"' is not assignable to parameter of type '"id" | "name" | "address" | "address.street" | "address.city"'.

TypeScript detects potential errors during development.

Important Considerations:

  1. This type implementation may impact performance with complex nested objects.
  2. The type system enhances development-time safety without runtime overhead.
  3. Consider the trade-off between type safety and code readability.

Wrap-Up

We’ve explored how to extract all keys from nested TypeScript objects. This technique provides enhanced type safety for your data structures. Consider the performance implications when implementing this in your projects.

Questions or thoughts?

Follow me on X for more TypeScript, Vue, and web dev insights! Feel free to DM me with:

  • Questions about this article
  • Topic suggestions
  • Feedback or improvements
Connect on X

Related Posts