Skip to main content

Як обмежити generic-тип ключами іншого типу?

1) Базовий патерн: ключ об'єкта

javascript
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 user
  • keyof T - об'єднання рядкових/числових/символьних ключів типу T.

2) Дозволити лише підмножину ключів

Іноді потрібні не всі ключі T, а лише частина:

javascript
function getIdOrName< T, K extends keyof T & ("id" | "name") >(obj: T, key: K): T[K] { return obj[key]; }
  • Тут K - перетин «ключі T» з конкретною множиною "id" | "name".

3) Ключі, чиї значення відповідають типу (фільтрація за значенням)

javascript
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):

javascript
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) Обмеження ключами індексної сигнатури

Якщо тип має словникову форму:

javascript
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) Ключі масивів і кортежів

javascript
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

Щоб отримати літеральні ключі, а не «розширені»:

javascript
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 / об'єктах-довідниках

javascript
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 з обмеженими ключами

javascript
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:

    javascript
    const 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).

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.