Skip to content
29 / 36

Как создать тип для путей вложенного объекта (Path<T>)?


Полный ответ

Базовая реализация:

TypeScript
type Path<T, Prefix extends string = ""> = T extends object
? {
  [K in keyof T & string]: Prefix extends ""
  ? K | Path<T[K], K>
  : `${Prefix}.${K}` | Path<T[K], `${Prefix}.${K}`>;
}[keyof T & string]
: never;

interface Settings {
  user: {
    name: string;
    address: {
      city: string;
      zip: string;
    };
  };
  theme: string;
}

type SettingsPath = Path<Settings>;
// "user" | "user.name" | "user.address" | "user.address.city" | "user.address.zip" | "theme"

Более чистая реализация через два шага:

TypeScript
type Path<T> = T extends object
? {
  [K in keyof T & string]: K | `${K}.${Path<T[K]>}`;
}[keyof T & string]
: never;

type SettingsPath = Path<Settings>;
// "user" | "theme" | "user.name" | "user.address" | "user.address.city" | "user.address.zip"

Получение типа значения по пути:

TypeScript
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends keyof T
? T[P]
: never;

type Test1 = PathValue<Settings, "user.name">;          // string
type Test2 = PathValue<Settings, "user.address.city">;   // string
type Test3 = PathValue<Settings, "theme">;               // string
type Test4 = PathValue<Settings, "user.address">;        // { city: string; zip: string }

Типобезопасный getter:

TypeScript
function get<T, P extends Path<T> & string>(
  obj: T,
  path: P
): PathValue<T, P> {
  return path.split(".").reduce(
    (current: unknown, key) => (current as Record<string, unknown>)[key],
    obj
  ) as PathValue<T, P>;
}

const settings: Settings = {
  user: { name: "Alice", address: { city: "Moscow", zip: "101000" } },
  theme: "dark",
};

const name = get(settings, "user.name");           // string
const city = get(settings, "user.address.city");   // string
// get(settings, "user.nonexistent");               // ошибка компиляции

Ограничение глубины рекурсии (для больших объектов):

TypeScript
type Path<T, Depth extends unknown[] = []> = Depth["length"] extends 5
? never // ограничиваем глубину 5 уровнями
: T extends object
? {
  [K in keyof T & string]: K | `${K}.${Path<T[K], [...Depth, unknown]>}`;
}[keyof T & string]
: never;

Реальные кейсы

Типобезопасные формы:

TypeScript
interface FormData {
  personal: { firstName: string; lastName: string };
  contact: { email: string; phone: string };
}

function setField<P extends Path<FormData> & string>(
  form: FormData,
  path: P,
  value: PathValue<FormData, P>
): void {
  // установка значения по пути
  const keys = path.split(".");
  let target: Record<string, unknown> = form;
  for (let i = 0; i < keys.length - 1; i++) {
    target = target[keys[i]] as Record<string, unknown>;
  }
  target[keys[keys.length - 1]] = value;
}

setField(form, "personal.firstName", "Bob");    // OK
// setField(form, "personal.firstName", 42);    // ошибка: number не string
// setField(form, "personal.middleName", "X");  // ошибка: путь не существует

Ошибка: рекурсия без ограничения на массивах:

TypeScript
// ❌ Массивы дают бесконечные числовые ключи
interface Data {
  items: { name: string }[];
}

// Path<Data> попытается развернуть number -> ...

// ✅ Исключайте массивы или обрабатывайте их отдельно
type Path<T> = T extends (infer U)[]
? `${number}` | `${number}.${Path<U>}`
: T extends object
? { [K in keyof T & string]: K | `${K}.${Path<T[K]>}` }[keyof T & string]
: never;

Резюме

Path<T> — мощный рекурсивный тип на основе template literal types. В паре с PathValue<T, P> он позволяет создать полностью типобезопасные getter/setter по строковому пути. Ограничивайте глубину рекурсии и обрабатывайте массивы отдельно, чтобы не перегружать компилятор.

Как создать тип для путей вложенного объекта (Path<T>)? | JScriptiser