Як обмежити generic-тип ключами іншого типу?
1) Базовий патерн: ключ об'єкта
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Tim" };
getProp(user, "id"); // OK
getProp(user, "age"); // Помилка: "age" ∉ keyof typeof userkeyof T- об'єднання рядкових/числових/символьних ключів типуT.
2) Дозволити лише підмножину ключів
Іноді потрібні не всі ключі T, а лише частина:
function getIdOrName<
T,
K extends keyof T & ("id" | "name")
>(obj: T, key: K): T[K] {
return obj[key];
}- Тут
K- перетин «ключі T» з конкретною множиною"id" | "name".
3) Ключі, чиї значення відповідають типу (фільтрація за значенням)
type KeysOfType<T, V> = {
[K in keyof T]-?: T[K] extends V ? K : never
}[keyof T];
function getNumberField<
T,
K extends KeysOfType<T, number>
>(obj: T, key: K): number {
return obj[key] as number;
}
type U = { a: number; b: string; c: number };
/* K = "a" | "c" */Схожий прийом часто називають value-based key filtering.
4) Обов'язкові/необов'язкові/незаписувані ключі
Іноді потрібно обмежити ключі за «кваліфікатором» (optional/readonly):
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T];
type WritableKeys<T> = {
[K in keyof T]-?: { [P in K]: T[P] } extends { -readonly [P in K]: T[P] } ? K : never
}[keyof T];
function takeRequired<T, K extends RequiredKeys<T>>(obj: T, key: K) {
return obj[key];
}5) Обмеження ключами індексної сигнатури
Якщо тип має словникову форму:
type Dict = Record<string, number>; // індексна сигнатура
function getDict<
T extends Record<string, unknown>,
K extends keyof T
>(obj: T, key: K) {
return obj[key];
}
keyof Tпри індексній сигнатурі ≈string | number | symbol(залежить від ключів).
6) Ключі масивів і кортежів
function first<T extends readonly unknown[]>(arr: T) {
const i: Extract<keyof T, number> = 0; // лише числові ключі
return arr[i]; // T[number]
}У масиву keyof T включає й методи ("length", "push"). Щоб обмежитися індексами, перетинайте з number.
7) Ключі в літеральних об'єктах: as const
Щоб отримати літеральні ключі, а не «розширені»:
const cfg = {
api: "/api",
cdn: "/cdn",
} as const;
type Cfg = typeof cfg; // { readonly api: "/api"; readonly cdn: "/cdn" }
type CfgKey = keyof Cfg; // "api" | "cdn"
function fromCfg<K extends keyof Cfg>(k: K): Cfg[K] {
return cfg[k];
}8) Ключі в enum / об'єктах-довідниках
enum Role { Admin = "admin", User = "user" }
const ROLES = { Admin: "admin", User: "user" } as const;
type RoleKey = keyof typeof ROLES; // "Admin" | "User"
type RoleVal = typeof ROLES[RoleKey]; // "admin" | "user"
function allow<K extends keyof typeof ROLES>(k: K) { /* ... */ }9) Утиліти для збирання API: Pick/Record/Omit з обмеженими ключами
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const out = {} as Pick<T, K>;
keys.forEach(k => (out[k] = obj[k]));
return out;
}
function mapValues<T, K extends keyof T>(
obj: T, keys: K[], fn: (v: T[K], k: K) => T[K]
): Pick<T, K> {
const res = {} as Pick<T, K>;
for (const k of keys) res[k] = fn(obj[k], k);
return res;
}10) Часті граблі та підказки
-
keyof never->never: якщоTможе бутиnever,Kвже невивідний; додавайте захист. -
У масивів не забувайте фільтрувати ключі до
number, інакше з'являться рядкові методи. -
Якщо ключі приходять зі значення (масиву рядків), надавайте перевагу
as const:javascriptconst allowed = ["id", "name"] as const; type Allowed = typeof allowed[number]; // "id" | "name" function f<T, K extends keyof T & Allowed>(obj: T, key: K) {}
Резюме
- База:
K extends keyof T- ключіT. - Підмножина:
K extends keyof T & ("a" | "b"). - За типом значень:
KeysOfType<T, V>. - Для масивів:
Extract<keyof T, number>. - Для літералів/enums:
keyof typeof X(+as const).
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.