Skip to content
30 / 36

Как Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ get ΠΏΠΎ строковому ΠΏΡƒΡ‚ΠΈ (lodash.get)?


ΠŸΠΎΠ»Π½Ρ‹ΠΉ ΠΎΡ‚Π²Π΅Ρ‚

Полная рСализация с ΠΏΠΎΠ΄Π΄Π΅Ρ€ΠΆΠΊΠΎΠΉ массивов:

TypeScript
// Π¨Π°Π³ 1: гСнСрация всСх допустимых ΠΏΡƒΡ‚Π΅ΠΉ
type Path<T> = T extends ReadonlyArray<infer U>
? `${number}` | `${number}.${Path<U>}`
: T extends object
? { [K in keyof T & string]: K | `${K}.${Path<T[K]>}` }[keyof T & string]
: never;

// Π¨Π°Π³ 2: ΠΏΠΎΠ»ΡƒΡ‡Π΅Π½ΠΈΠ΅ Ρ‚ΠΈΠΏΠ° значСния ΠΏΠΎ ΠΏΡƒΡ‚ΠΈ
type PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}`
? K extends `${number}`
? T extends ReadonlyArray<infer U>
? PathValue<U, Rest>
: never
: K extends keyof T
? PathValue<T[K], Rest>
: never
: P extends `${number}`
? T extends ReadonlyArray<infer U>
? U
: never
: P extends keyof T
? T[P]
: never;

// Π¨Π°Π³ 3: рСализация get

function get<T, P extends Path<T> & string>(
  obj: T,
  path: P
): PathValue<T, P> | undefined {
  const keys = path.split(".");
  let current: unknown = obj;

  for (const key of keys) {
    if (current === null || current === undefined) return undefined;
    current = (current as Record<string, unknown>)[key];
  }

  return current as PathValue<T, P> | undefined;
}

ИспользованиС:

TypeScript
interface Company {
  name: string;
  ceo: {
    firstName: string;
    lastName: string;
    contacts: {
      email: string;
      phones: string[];
    };
  };
  departments: Array<{
    title: string;
    headcount: number;
  }>;
}

const company: Company = {
  name: "Acme",
  ceo: {
    firstName: "Alice",
    lastName: "Smith",
    contacts: { email: "alice@acme.com", phones: ["+1234567890"] },
  },
  departments: [
    { title: "Engineering", headcount: 50 },
    { title: "Marketing", headcount: 20 },
  ],
};

const email = get(company, "ceo.contacts.email");  // string | undefined
const name = get(company, "name");                   // string | undefined
const dept = get(company, "departments.0.title");    // string | undefined

// get(company, "ceo.age");        // ошибка: "ceo.age" нС Path<Company>
// get(company, "nonexistent");    // ошибка

ВСрсия с default value:

TypeScript
function getOr<T, P extends Path<T> & string, D>(
  obj: T,
  path: P,
  defaultValue: D
): NonNullable<PathValue<T, P>> | D {
  const result = get(obj, path);
  return (result ?? defaultValue) as NonNullable<PathValue<T, P>> | D;
}

const phone = getOr(company, "ceo.contacts.phones.0", "N/A"); // string

ВипобСзопасный set:

TypeScript
function set<T extends object, P extends Path<T> & string>(
  obj: T,
  path: P,
  value: PathValue<T, P>
): T {
  const keys = path.split(".");
  const result = structuredClone(obj);
  let current: Record<string, unknown> = result as Record<string, unknown>;

  for (let i = 0; i < keys.length - 1; i++) {
    const next = current[keys[i]];
    if (typeof next === "object" && next !== null) {
      const clone = Array.isArray(next) ? [...next] : { ...next };
      current[keys[i]] = clone;
      current = clone as Record<string, unknown>;
    }
  }

  current[keys[keys.length - 1]] = value;
  return result;
}

const updated = set(company, "ceo.firstName", "Bob"); // updated.ceo.firstName === "Bob", ΠΎΡ€ΠΈΠ³ΠΈΠ½Π°Π» Π½Π΅ ΠΈΠ·ΠΌΠ΅Π½Ρ‘Π½

Π Π΅Π°Π»ΡŒΠ½Ρ‹Π΅ кСйсы

ВипобСзопасный i18n:

TypeScript
const translations = {
  header: { title: "Welcome", subtitle: "To our app" },
  buttons: { submit: "Submit", cancel: "Cancel" },
} as const;

type Translations = typeof translations;

function t<P extends Path<Translations> & string>(key: P): PathValue<Translations, P> {
  return get(translations, key) as PathValue<Translations, P>;
}

const title = t("header.title");     // "Welcome" (literal type!) // t("header.nonexistent");          // ошибка компиляции

Ошибка: Π·Π°Π±Ρ‹Ρ‚ΡŒ ΠΏΡ€ΠΎ undefined Π² Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌΠΎΠΌ Ρ‚ΠΈΠΏΠ΅:

TypeScript
// ❌ Если ΠΎΠ±ΡŠΠ΅ΠΊΡ‚ ΠΏΡ€ΠΈΡˆΡ‘Π» ΠΈΠ· API, Π²Π»ΠΎΠΆΠ΅Π½Π½Ρ‹Π΅ поля ΠΌΠΎΠ³ΡƒΡ‚ Π±Ρ‹Ρ‚ΡŒ undefined

function unsafeGet<T, P extends Path<T> & string>(
  obj: T,
  path: P
): PathValue<T, P> {
  // runtime crash, Ссли ΠΏΡ€ΠΎΠΌΠ΅ΠΆΡƒΡ‚ΠΎΡ‡Π½Ρ‹ΠΉ ΠΊΠ»ΡŽΡ‡ undefined
  return path.split(".").reduce((c: unknown, k) => (c as Record<string, unknown>)[k], obj) as PathValue<T, P>;
}

// βœ… ВсСгда Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°ΠΉΡ‚Π΅ T | undefined

function safeGet<T, P extends Path<T> & string>(
  obj: T,
  path: P
): PathValue<T, P> | undefined {
  // ...с ΠΏΡ€ΠΎΠ²Π΅Ρ€ΠΊΠΎΠΉ null/undefined Π½Π° ΠΊΠ°ΠΆΠ΄ΠΎΠΌ шагС
}

РСзюмС

ВипобСзопасный get опираСтся Π½Π° Path<T> для автодополнСния ΠΈ PathValue<T, P> для Π²Ρ‹Π²ΠΎΠ΄Π° Ρ‚ΠΈΠΏΠ° Ρ€Π΅Π·ΡƒΠ»ΡŒΡ‚Π°Ρ‚Π°. Template literal types с infer Ρ€Π°Π·Π±ΠΈΡ€Π°ΡŽΡ‚ строку "a.b.c" ΠΏΠΎ Ρ‚ΠΎΡ‡ΠΊΠ°ΠΌ Π½Π° ΡƒΡ€ΠΎΠ²Π½Π΅ Ρ‚ΠΈΠΏΠΎΠ². ВсСгда ΡƒΡ‡ΠΈΡ‚Ρ‹Π²Π°ΠΉΡ‚Π΅ undefined Π² Π²ΠΎΠ·Π²Ρ€Π°Ρ‰Π°Π΅ΠΌΠΎΠΌ Ρ‚ΠΈΠΏΠ΅ ΠΈ ΠΎΠ±Ρ€Π°Π±Π°Ρ‚Ρ‹Π²Π°ΠΉΡ‚Π΅ массивы Ρ‡Π΅Ρ€Π΅Π· числовыС индСксы.

Как Ρ‚ΠΈΠΏΠΈΠ·ΠΈΡ€ΠΎΠ²Π°Ρ‚ΡŒ Ρ„ΡƒΠ½ΠΊΡ†ΠΈΡŽ get ΠΏΠΎ строковому ΠΏΡƒΡ‚ΠΈ (lodash.get)? | JScriptiser