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 Π² Π²ΠΎΠ·Π²ΡΠ°ΡΠ°Π΅ΠΌΠΎΠΌ ΡΠΈΠΏΠ΅ ΠΈ ΠΎΠ±ΡΠ°Π±Π°ΡΡΠ²Π°ΠΉΡΠ΅ ΠΌΠ°ΡΡΠΈΠ²Ρ ΡΠ΅ΡΠ΅Π· ΡΠΈΡΠ»ΠΎΠ²ΡΠ΅ ΠΈΠ½Π΄Π΅ΠΊΡΡ.