Skip to content

Instantly share code, notes, and snippets.

@okaybeydanol
Created February 8, 2025 01:16
Show Gist options
  • Select an option

  • Save okaybeydanol/8ad2a4218b733d931a553cddd5ba3bf8 to your computer and use it in GitHub Desktop.

Select an option

Save okaybeydanol/8ad2a4218b733d931a553cddd5ba3bf8 to your computer and use it in GitHub Desktop.
TypeScript: Extract Nested Object Keys with Depth Limit (With and Without Infer)
/**
* Generates a tuple of numbers from 0 up to (but not including) N.
* Used internally for numeric operations like subtraction.
*
* Example:
* type Numbers = Enumerate<3>; // [0, 1, 2]
*/
type Enumerate<
N extends number,
Acc extends number[] = [],
> = Acc['length'] extends N ? Acc : Enumerate<N, [...Acc, Acc['length']]>;
/**
* Subtracts two numbers at the type level.
* Used internally for limiting the depth of nested paths.
*
* Example:
* type Result = Subtract<5, 2>; // 3
*/
type Subtract<A extends number, B extends number> = Enumerate<A> extends [
...Enumerate<B>,
...infer R,
]
? R['length']
: 0;
/**
* Recursively extracts nested keys of an object as dot-notation strings with a depth limit.
* Useful for representing nested object paths in a string format with a maximum depth.
*
* Example:
* type Theme = { colors: { primary: string; secondary: { light: string; dark: string } } };
* type Paths = NestedKeyPathsWithDepth<Theme['colors'], 2>;
* "primary" | "secondary.light" | "secondary.dark"
*/
export type NestedKeysPathsWithDepth<T, Depth extends number = 1> = [
Depth,
] extends [0]
? never // Base case: if depth is 0, stop recursion
: T extends object
? {
[K in keyof T & (string | number)]: T[K] extends Record<string | number, unknown>
? Subtract<Depth, 1> extends 0
? never // Stop recursion if depth limit is reached
: `${K}.${NestedKeysPathsWithDepth<T[K], Subtract<Depth, 1>>}` // Recursive case: dive deeper
: never; // Invalid case: neither string nor object
}[keyof T & (string | number)]
: never;
/**
* Recursively extracts nested keys of an object as dot-notation strings with a depth limit using `infer`.
* Useful for representing nested object paths in a string format with a maximum depth.
*
* Example:
* type Theme = { colors: { primary: string; secondary: { light: string; dark: string } } };
* type Paths = NestedKeyPathsWithDepthInfer<Theme['colors'], 2>;
* "primary" | "secondary.light" | "secondary.dark"
*/
export type NestedKeysPathsWithDepthInfer<T, Depth extends number = 1> = [
Depth,
] extends [0]
? never // Base case: if depth is 0, stop recursion
: T extends object
? {
[K in keyof T & (string | number)]: T[K] extends infer V
? V extends Record<string | number, unknown>
? Subtract<Depth, 1> extends 0
? never // Stop recursion if depth limit is reached
: `${K}.${NestedKeysPathsWithDepthInfer<V, Subtract<Depth, 1>>}` // Recursive case: dive deeper
: V extends string
? `${K}` // Base case: if the value is a string, return the key
: never // Invalid case: neither string nor object
: never; // Invalid case: key is not a string or number
}[keyof T & (string | number)]
: never;
@okaybeydanol
Copy link
Author

Two utility types for extracting nested keys as dot-notation strings with a depth limit:

  1. Direct Access: Simpler but less flexible.
  2. Infer Version: Uses infer for better type control.

Example:

type Theme = {
  colors: {
    primary: string;
    secondary: { light: string; dark: string };
  };
};

type PathsDepth = NestedKeyPathsWithDepth<Theme['colors'], 2>;
// "primary" | "secondary.light" | "secondary.dark"

type PathsDepthInfer = NestedKeyPathsWithDepthInfer<Theme['colors'], 2>;
// "primary" | "secondary.light" | "secondary.dark"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment