Hone logo
Hone
Problems

TypeScript: Generate a Path Union Type from an Object

This challenge focuses on creating a type-safe utility in TypeScript that can generate a union of all possible dot-separated paths to leaf properties within a given object structure. This is incredibly useful for scenarios like creating type-safe configuration readers, form field selectors, or deeply nested state accessors.

Problem Description

Your task is to create a TypeScript utility type named Path that takes a generic object type T as input and returns a union of strings representing all possible dot-separated paths to the primitive (non-object, non-array) values within T.

Key Requirements

  1. The Path type should recursively traverse the input object T.
  2. It should generate strings that represent the full path to each leaf node.
  3. Leaf nodes are considered properties whose values are not objects or arrays. Primitive types like string, number, boolean, null, undefined, symbol, and bigint are leaf nodes.
  4. Paths should be joined by dots (e.g., "a.b.c").
  5. Handle nested objects.
  6. Handle arrays: Treat array indices as part of the path (e.g., "arr.0.prop").

Expected Behavior

Given an object type, Path<T> should produce a union of all valid string paths to its primitive values.

Edge Cases

  • Empty objects.
  • Objects with null or undefined values.
  • Objects with empty arrays.
  • Objects with deeply nested structures.

Examples

Example 1:

type Obj1 = {
  a: string;
  b: {
    c: number;
    d: boolean;
  };
};

type Path1 = Path<Obj1>;
// Expected Output: "a" | "b.c" | "b.d"

Explanation: The paths lead to "a" (string), "b.c" (number), and "b.d" (boolean).

Example 2:

type Obj2 = {
  name: string;
  address: {
    street: string;
    city: string;
    zip: number | null;
  };
  tags: string[];
  config: {
    enabled: boolean;
    options: {
      timeout: number;
      retries: number | undefined;
    }[];
  };
};

type Path2 = Path<Obj2>;
// Expected Output:
// "name" |
// "address.street" |
// "address.city" |
// "address.zip" |
// "tags.0" |
// "tags.1" | // ... and so on for any number of array elements, but the type only needs to represent the structure
// "config.enabled" |
// "config.options.0.timeout" |
// "config.options.0.retries" |
// "config.options.1.timeout" |
// "config.options.1.retries" | // ... and so on for any number of array elements

Explanation: This example shows paths to primitives within nested objects and also paths into arrays. For array elements, we represent the structure with an index, 0, 1, etc., indicating that any index within the array can lead to a primitive.

Example 3:

type Obj3 = {
  data: {
    items: Array<{ id: number; value: string } | null>;
    metadata: {
      timestamp: bigint;
      isValid: boolean | null;
    };
  };
  settings: {}; // Empty object
};

type Path3 = Path<Obj3>;
// Expected Output:
// "data.items.0.id" |
// "data.items.0.value" |
// "data.metadata.timestamp" |
// "data.metadata.isValid"

Explanation: This covers null values in arrays, empty objects, and various primitive types. Note that data.items.0 and data.items.1 etc. would lead to null or an object, but we only care about paths that end in primitives. The type Array<{ id: number; value: string } | null> implies that an element at index 0 could be an object with id or value.

Constraints

  • The solution must be a TypeScript utility type.
  • The solution should be reasonably efficient for typical object structures, avoiding excessive recursive depth where possible.
  • Input T will always be an object type (or an array type, which is treated as an object with numeric keys).

Notes

  • Consider how to differentiate between primitive types and object/array types in TypeScript.
  • Think about the base cases for your recursion.
  • For arrays, you need a way to represent any index. The common convention is to use number or 0, 1, etc., to signify an array index path segment. For this challenge, generating paths with a representative index like 0 is sufficient to demonstrate the concept of array traversal. If an array can contain objects with properties, you'll need to recursively generate paths into those objects as well.
  • You might need multiple conditional types and recursive calls to handle all the nuances of object and array traversal.
  • Pay attention to the never type – it's often the result of type-level operations that don't yield valid paths.
Loading editor...
typescript