Skip to main content

Як працюють keyof та typeof у TypeScript

keyof витягує імена властивостей типу у вигляді union-типу рядкових літералів. typeof фіксує тип будь-якого значення під час компіляції, щоб не оголошувати той самий тип двічі.

Теорія

TL;DR

  • keyof Type повертає union з іменами ключів: keyof Person дає "name" | "age"
  • typeof value повертає виведений тип значення: typeof person дає { name: string; age: number }
  • keyof схожий на список кнопок на торговому автоматі: показує допустимі варіанти. typeof схожий на читання наліпки на тому, що вже тримаєш у руках
  • Використовуй K extends keyof T, щоб обмежити параметр функції до реальних ключів; typeof obj — щоб не дублювати наявний тип
  • Разом як keyof typeof obj дають union ключів прямо зі значення, без окремого оголошення типу

Короткий приклад

typescript
type Person = { name: string; age: number; }; type Keys = keyof Person; // "name" | "age" const person = { name: "Alice", age: 30 }; type PersonType = typeof person; // { name: string; age: number } const key: Keys = "name"; // OK // const bad: Keys = "email"; // Помилка: "email" не входить у "name" | "age"

keyof обмежує key до реальних властивостей типу Person. typeof копіює форму об'єкта person без ручного оголошення типу.

Головна різниця

keyof працює з типами (інтерфейси, type-аліаси, класи) і повертає union імен властивостей. typeof працює зі значеннями (змінні, вирази, посилання на функції) і повертає тип, який можна використати там, де очікується тип. Вони вирішують протилежні задачі: typeof відповідає на питання "який тип має це значення?", keyof — "які ключі є у цього типу?". Разом вони дозволяють перейти від звичайного об'єкта до типобезпечного union ключів за один крок.

Коли використовувати

  • Generic-доступ до властивостей: K extends keyof T, щоб obj[key] завжди залишався типобезпечним і повернений тип був точним
  • Уникнути дублювання типу для існуючого об'єкта або константи: type Config = typeof config
  • Ітерація або маппінг по ключах: keyof у mapped types — { [K in keyof T]: ... }
  • Виведення типів пропсів компонента або функції: Parameters<typeof Button>[0]
  • Обмежити рядковий аргумент до допустимих імен властивостей: (key: keyof User) => User[key]

Порівняння

keyoftypeof
ВхідТипЗначення або вираз
ВихідUnion рядкових літералів (імена ключів)Виведений тип значення
Працює зІнтерфейси, type-аліаси, класиЗмінні, об'єкти, функції, вирази
Вартість у рантайміВідсутняВідсутня
Типова комбінаціяT[K] (indexed access)keyof typeof obj
Типове застосуванняОбмеження параметрів-ключівПовторне використання форми об'єкта

Як це обробляє компілятор

Обидва оператори існують лише на рівні компіляції — жодного коду вони не генерують. Коли TypeScript обробляє keyof T, він проходить по оголошеннях властивостей типу і будує union рядкових літералів. Для typeof expr тайп-чекер розраховує тип виразу через аналіз потоку даних і фіксує результат як тип-аліас. Важливий нюанс: let розширює (widen) тип. const y = 5 дає typeof y як 5 (літеральний тип). let y = 5 дає number. Починаючи з TypeScript 4.1, keyof також включає symbol-ключі, тому результат може містити string | number | symbol.

Типові помилки

keyof на примітивному типі

typescript
type S = keyof string; // number | typeof Symbol.iterator | "toString" | "charAt" | ... // Це всі члени прототипу рядка, зазвичай не те, що потрібно type OK = keyof { x: string }; // "x"

Застосовуй keyof до власних типів, а не до вбудованих примітивів.

typeof на неініціалізованій змінній

typescript
let x; type T = typeof x; // any — жодної корисної інформації const y = 5; type U = typeof y; // 5 (літеральний тип, набагато корисніше)

Оголошуй з const і ініціалізуй перед тим, як використовувати typeof.

keyof typeof на порожньому об'єкті

typescript
let obj = {}; type K = keyof typeof obj; // string | number | symbol const typed: { a: number } = { a: 1 }; type K2 = keyof typeof typed; // "a"

Порожній об'єкт {} у TypeScript має неявну index signature. keyof typeof {} повертає тип цієї сигнатури, а не never. Використовуй const і вказуй конкретні властивості.

Symbol-ключі у TypeScript 4.1+

typescript
// Щоб отримати лише рядкові ключі з keyof: type StringKeys = Extract<keyof T, string>;

До версії 4.1 keyof повертав тільки рядкові ключі. З 4.1 результат може включати symbol-ключі.

Де зустрічається

  • Zod: type Schema = z.infer<typeof userSchema> — runtime-валідатор стає TypeScript-типом
  • React: type ButtonProps = Parameters<typeof Button>[0] — типи пропсів без ручного оголошення
  • Redux Toolkit: type State = ReturnType<typeof store.getState>
  • Будь-який generic-доступ до властивостей: function get<T, K extends keyof T>(obj: T, key: K): T[K]
  • Mapped types: Record<keyof Config, string> для трансформації всіх ключів наявного типу

Питання на співбесіді

Q: Що повертає keyof T[K]?
A: Ключі типу, який стоїть за T[K]. Якщо T = { user: { name: string } } і K = "user", то keyof T[K] це "name".

Q: Як keyof поводиться з intersection-типами (перетин типів)?
A: keyof (A & B) дає keyof A | keyof B. Intersection містить властивості обох сторін, тому keyof збирає їх усі.

Q: Чи працює typeof на функціях з overload-ами?
A: Так, але захоплює всю сукупність сигнатур. Щоб виділити конкретну частину, використовуй Parameters<typeof fn> або ReturnType<typeof fn>.

Q: Що змінилось у TypeScript 4.1 для keyof?
A: До версії 4.1 keyof повертав лише рядкові ключі. З 4.1 результат може включати symbol-ключі.

Q (senior): Розробник написав keyof typeof obj, де obj = {} оголошено через let. Очікував never, отримав string | number | symbol. Чому?
A: Порожній об'єкт {} у TypeScript має неявну index signature, яка приймає будь-який рядковий, числовий або symbol-ключ. Тому keyof typeof {} повертає тип цієї сигнатури. Щоб отримати never, потрібен тип без жодних властивостей, наприклад Record<never, never>.

Приклади

Базовий: типобезпечний доступ до властивостей

typescript
type User = { id: number; name: string }; function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } const user: User = { id: 1, name: "Bob" }; const name = getProp(user, "name"); // тип string, виводиться автоматично // getProp(user, "email"); // Помилка: "email" немає у User console.log(name); // "Bob"

K extends keyof T говорить TypeScript, що key має бути реальною властивістю obj. Тип, що повертається, T[K] точно відповідає типу цієї властивості. Ніяких type assertion, ніякого any.

Середній: конфіг з виведеними типами

typescript
const config = { host: "localhost", port: 3000, debug: true, } as const; type Config = typeof config; // { readonly host: "localhost"; readonly port: 3000; readonly debug: true } type ConfigKey = keyof Config; // "host" | "port" | "debug" function readConfig(key: ConfigKey): Config[typeof key] { return config[key]; } readConfig("host"); // "localhost" // readConfig("url"); // Помилка: "url" не входить у ConfigKey

as const зберігає літеральні типи замість їх розширення. typeof config фіксує форму об'єкта, keyof обмежує допустимі ключі. Я особисто бачив, як цей патерн відловлює опечатки у зверненнях до конфігу, які інакше прийшли б тільки в рантаймі.

Просунутий: типобезпечна система подій

typescript
const handlers = { userCreated: (id: number) => void 0, userDeleted: (id: number) => void 0, pageLoaded: () => void 0, }; type Handlers = typeof handlers; type EventName = keyof Handlers; // "userCreated" | "userDeleted" | "pageLoaded" function emit<K extends EventName>(name: K, ...args: Parameters<Handlers[K]>): void { (handlers[name] as (...a: unknown[]) => void)(...args); } emit("userCreated", 42); // OK // emit("userCreated"); // Помилка: відсутній аргумент // emit("unknown", 42); // Помилка: "unknown" не є допустимою назвою події

keyof typeof handlers дає union імен подій. Parameters<Handlers[K]> витягує список аргументів для конкретного обробника. І назва події, і її аргументи перевіряються на рівні типів ще до запуску коду.

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

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

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

Дочитали статтю?