Skip to content

Instantly share code, notes, and snippets.

@okaybeydanol
Created February 8, 2025 22:48
Show Gist options
  • Save okaybeydanol/ff6cce734ce50c3edfbdae4b10ca3c18 to your computer and use it in GitHub Desktop.
Save okaybeydanol/ff6cce734ce50c3edfbdae4b10ca3c18 to your computer and use it in GitHub Desktop.
TypeScript: Extract Nested Object Keys as Arrays (Basic, Infer, and String-Enforced)
/**
* Recursively extracts nested keys of an object as arrays.
* Useful for representing nested object paths in an array format.
*
* Example:
* type Theme = { colors: { primary: string; secondary: { light: string; dark: string } } };
* type Paths = NestedKeyArray<Theme['colors']>;
* ["primary"] | ["secondary", "light"] | ["secondary", "dark"]
*/
export type NestedKeysArray<T> = T extends object
? {
[K in keyof T]: K extends string | number
? T[K] extends string
? [K] // Base case: if the value is a string, return the key as a single-element array
: T[K] extends object
? [K, ...NestedKeysArray<T[K]>] // Recursive case: if the value is an object, dive deeper
: never // Invalid case: neither string nor object
: never; // Invalid case: key is not a string or number
}[keyof T]
: never;
/**
* Recursively extracts nested keys of an object as arrays using `infer`.
* Useful for representing nested object paths in an array format.
*
* Example:
* type Theme = { colors: { primary: string; secondary: { light: string; dark: string } } };
* type Paths = NestedKeysArrayInfer<Theme['colors']>;
* ["primary"] | ["secondary", "light"] | ["secondary", "dark"]
*/
export type NestedKeysArrayInfer<T> = T extends object
? {
[K in keyof T]: T[K] extends infer V
? V extends Record<string, unknown>
? [K, ...NestedKeysArrayInfer<T[K]>] // Recursive case: if the value is an object, 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]
: never;
/**
* Recursively extracts nested keys of an object as arrays using `infer` and ensures keys are strings.
* Useful for representing nested object paths in an array format.
*
* Example:
* type Theme = { colors: { primary: string; secondary: { light: string; dark: string } } };
* type Paths = NestedKeysArrayInferString<Theme['colors']>;
* ["primary"] | ["secondary", "light"] | ["secondary", "dark"]
*/
export type NestedKeysArrayInferString<T> = T extends object
? {
[K in keyof T]: K extends infer N extends string
? T[K] extends string
? [N] // Base case: if the value is a string, return the key
: [N, ...NestedKeysArrayInferString<T[K]>] // Recursive case: if the value is an object, dive deeper
: T[K] extends string
? [K] // Base case: if the value is a string, return the key
: [K, ...NestedKeysArrayInferString<T[K]>]; // Recursive case: if the value is an object, dive deeper
}[keyof T]
: never;
@okaybeydanol
Copy link
Author

Three utility types for recursively extracting nested object keys as arrays:

  1. Basic Version: Directly accesses nested keys without infer.
  2. Infer Version: Uses infer for value type extraction.
  3. String-Enforced Version: Ensures keys are strings using infer.

Examples:

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

// Basic
type PathsBasic = NestedKeyArray<Theme['colors']>; 
// ["primary"] | ["secondary", "light"] | ["secondary", "dark"]

// Infer
type PathsInfer = NestedKeysArrayInfer<Theme['colors']>; 
// Same as above

// String-Enforced
type PathsString = NestedKeysArrayInferString<Theme['colors']>; 
// ["primary"] | ["secondary", "light"] | ["secondary", "dark"]

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