How to Recursively Extract All Keys from a TypeScript Interface with DeepKeyOf

This article explains how TypeScript's built‑in keyof operator can be extended with a recursive generic type called DeepKeyOf to enumerate every nested property path of an interface, illustrating the concept of type gymnastics, showing step‑by‑step implementations, and highlighting practical benefits for type‑safe code.

ELab Team
ELab Team
ELab Team
How to Recursively Extract All Keys from a TypeScript Interface with DeepKeyOf

Background

TypeScript provides the keyof keyword to obtain all top‑level keys of an interface, but it lacks a recursive way to retrieve nested keys. Such a capability is useful for scenarios like lodash.get, where path strings can be strongly typed instead of using plain string.

interface Stu {
  name: string;
  age: number;
}

type keys = keyof Stu; // type keys = 'name' | 'age'

For nested structures, we would like a deepkeyof that yields a union such as 'name' | 'age' | 'nest' | 'nest.a' | 'nest.a.b'.

interface Stu {
  name: string;
  age: number;
  nest: {
    a: {
      b: number;
    }
  }
}

// expected
// type deepKeys = 'name' | 'age' | 'nest' | 'nest.a' | 'nest.a.b'

Implementation Idea

1. What Is Type Gymnastics?

The term “type gymnastics” originated in Haskell documentation and can be understood as type programming : manipulating types as values. In JavaScript we manipulate runtime values with operators like +, *, or built‑in APIs such as split. In TypeScript, the “values” are types, and we use type operators like keyof to extract information.

type Temp = {
  name: string;
  age: number;
}

type keys = keyof Temp; // 'name' | 'age'

Type programming differs from ordinary programming in that variables are immutable (we create new types rather than modify existing ones), there are no statements—only expressions such as extends ? : —and loops are simulated with recursion.

// conditional type example
type IsNumber<T> = T extends number ? true : false;
type Res = IsNumber<10>; // true
// recursive string split example
type Split<T extends string> = T extends `${infer A}.${infer B}` ? A | Split<B> : T;
type Test = Split<'a.b.c'>; // 'a' | 'b' | 'c'

2. TypeScript Type Operations Used

keyof

Index Access Types – retrieve the type of a specific property, e.g., Stu['nest'] Template Literal Types – build string unions extends + infer – decompose a type generic + recursion – foundation for recursive types mapped type – construct a new type based on an existing one

Sequential Approach

Use keyof to get first‑level keys.

type keys1 = keyof Stu; // 'name' | 'age' | 'nest'

Use those keys to index the interface and obtain second‑level types.

type types2 = Stu[keys1]; // string | number | { a: { b: number } }

Filter out non‑object types.

type OnlyObject<T> = T extends Record<string, any> ? T : never;
type types2_needed = OnlyObject<types2>; // { a: { b: number } }

Extract keys of the second level. type keys2 = keyof types2_needed; // 'a'\n Repeat the process for deeper levels.

The sequential method fails to correctly concatenate keys from different levels because the context of each nested key is lost.

3. Recursive Solution

We design a generic DeepKeyOf that recursively builds a union of all property paths.

type DeepKeyOf<T> = T extends Record<string, any> ? {
  [k in keyof T]: k extends string ? k | `${k}.${DeepKeyOf<T[k]>}` : never;
}[keyof T] : never;

To avoid incompatibility between keyof T (which can be string | number | symbol) and template literals, we constrain k to string.

type DeepKeyOf<T> = T extends Record<string, any> ? {
  [k in keyof T]: k extends string ? k | `${k}.${DeepKeyOf<T[k]>}` : never;
}[keyof T] : never;

Applying it to the example interface yields:

type DeepKeyOf<T> = T extends Record<string, any> ? {
  [k in keyof T]: k extends string ? k | `${k}.${DeepKeyOf<T[k]>}` : never;
}[keyof T] : never;

interface Stu {
  name: string;
  nest: {
    a: { b: number };
    tt: { c: boolean };
  };
  info: { score: number; grade: string };
}

type Res = DeepKeyOf<Stu>; // "name" | "nest" | "info" | "nest.a" | "nest.tt" | "nest.a.b" | "nest.tt.c" | "info.score" | "info.grade"

Conclusion

The DeepKeyOf generic demonstrates how type programming can narrow types to the exact shape needed, improving IDE assistance, reducing runtime errors, and making code more expressive. Similar techniques can be used to enforce stricter string formats, such as integer‑only strings via `${bigint}`.

References

[1] Haskell documentation on type classes.

[2] Indexed Access Types – https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html

[3] Template Literal Types – https://www.typescriptlang.org/docs/handbook/2/template-literal-types.html

[4] Neutral element (never) – https://zh.wikipedia.org/zh-cn/%E5%96%AE%E4%BD%8D%E5%85%83

[5] Type Challenges repository – https://github.com/type-challenges/type-challenges/blob/master/README.md

TypeScriptGeneric Typestype programmingtype gymnasticskeyofDeepKeyOf
ELab Team
Written by

ELab Team

Sharing fresh technical insights

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.