Hone logo
Hone
Problems

Dynamic Path Mapping Types in TypeScript

This challenge focuses on creating flexible and type-safe path mapping utilities in TypeScript. You'll learn how to dynamically generate types that represent nested object structures, mirroring file system paths or API endpoint structures, ensuring compile-time safety for accessing these nested properties.

Problem Description

The goal is to create a set of TypeScript utility types that allow you to map string literal paths to specific values within a given object structure. This is analogous to how you might access nested properties in an object using string keys, but with the added benefit of TypeScript's static type checking.

Key Requirements:

  1. Path<T>: A type that takes a generic type T (representing an object structure) and returns a union of all possible string literal paths within T. For example, if T is { a: { b: number } }, Path<T> should resolve to "a" | "a.b".
  2. PathValue<T, P>: A type that takes a generic type T and a specific path string literal P (which must be a valid path within T) and returns the type of the value at that path. For example, if T is { a: { b: number } } and P is "a.b", PathValue<T, P> should resolve to number.
  3. PathMappings<T>: A type that takes a generic type T and generates an object where keys are all possible string literal paths from Path<T>, and values are the corresponding types from PathValue<T, Path>.

Expected Behavior:

The types should correctly handle nested objects, arrays, and primitive values. Paths should be delimited by dots (.).

Edge Cases:

  • Empty objects.
  • Objects with arrays (paths to array elements or their properties).
  • Objects with null or undefined values.
  • Paths that do not exist. (The utility types should ideally prevent this at compile time).

Examples

Example 1: Basic Nested Object

type Data = {
  user: {
    id: number;
    name: string;
    address: {
      street: string;
      city: string;
    };
  };
  posts: Array<{
    title: string;
    content: string;
  }>;
};

// Expected Path<Data> to be:
// "user" | "user.id" | "user.name" | "user.address" | "user.address.street" | "user.address.city" | "posts" | "posts.[]" | "posts.[].title" | "posts.[].content"

// Expected PathValue<Data, "user.name"> to be:
// string

// Expected PathValue<Data, "posts.[].title"> to be:
// string

// Expected PathMappings<Data> to be an object like:
// {
//   user: { id: number; name: string; address: { street: string; city: string; } };
//   "user.id": number;
//   "user.name": string;
//   "user.address": { street: string; city: string; };
//   "user.address.street": string;
//   "user.address.city": string;
//   posts: Array<{ title: string; content: string; }>;
//   "posts.[]": { title: string; content: string; }; // Representing an item in the array
//   "posts.[].title": string;
//   "posts.[].content": string;
// }

Example 2: Array of Objects with Nested Properties

type Config = {
  settings: {
    theme: 'light' | 'dark';
    notifications: Array<{
      type: string;
      enabled: boolean;
    }>;
  };
};

// Expected Path<Config> to be:
// "settings" | "settings.theme" | "settings.notifications" | "settings.notifications.[]" | "settings.notifications.[].type" | "settings.notifications.[].enabled"

// Expected PathValue<Config, "settings.notifications.[].enabled"> to be:
// boolean

Example 3: Edge Case - Empty Object

type Empty = {};

// Expected Path<Empty> to be: never

// Expected PathValue<Empty, "any.path"> to ideally be a compile-time error or infer unknown

Constraints

  • The solution must be written entirely in TypeScript.
  • The utility types should aim for good performance and not lead to excessive compiler overhead, especially with deeply nested objects.
  • Paths to array elements should be represented using a placeholder like [] or [*]. For this challenge, let's standardize on [] to represent any item within an array.
  • The Path<T> type should only include paths that lead to a non-object, non-array value, OR to the array itself, OR to an element within an array. It should also include paths to nested objects that might contain further properties.

Notes

  • This problem involves recursive type manipulation. Consider how you can break down the problem of traversing an object into smaller, repeatable steps.
  • Template literal types will be crucial for constructing and manipulating the string paths.
  • Conditional types and inference (infer) will be your allies in dissecting and rebuilding types.
  • Think about how to handle the base cases for your recursive types.
  • When dealing with arrays, you'll need a strategy to represent paths to the array itself, paths to any element within the array, and paths to properties within those elements.
  • The PathMappings<T> type is a synthesis of the previous two types.
Loading editor...
typescript