Як працюють 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 ключів прямо зі значення, без окремого оголошення типу
Короткий приклад
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]
Порівняння
keyof | typeof | |
|---|---|---|
| Вхід | Тип | Значення або вираз |
| Вихід | 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 на примітивному типі
type S = keyof string;
// number | typeof Symbol.iterator | "toString" | "charAt" | ...
// Це всі члени прототипу рядка, зазвичай не те, що потрібно
type OK = keyof { x: string }; // "x"Застосовуй keyof до власних типів, а не до вбудованих примітивів.
typeof на неініціалізованій змінній
let x;
type T = typeof x; // any — жодної корисної інформації
const y = 5;
type U = typeof y; // 5 (літеральний тип, набагато корисніше)Оголошуй з const і ініціалізуй перед тим, як використовувати typeof.
keyof typeof на порожньому об'єкті
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+
// Щоб отримати лише рядкові ключі з 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>.
Приклади
Базовий: типобезпечний доступ до властивостей
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.
Середній: конфіг з виведеними типами
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" не входить у ConfigKeyas const зберігає літеральні типи замість їх розширення. typeof config фіксує форму об'єкта, keyof обмежує допустимі ключі. Я особисто бачив, як цей патерн відловлює опечатки у зверненнях до конфігу, які інакше прийшли б тільки в рантаймі.
Просунутий: типобезпечна система подій
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]> витягує список аргументів для конкретного обробника. І назва події, і її аргументи перевіряються на рівні типів ще до запуску коду.
Коротка відповідь
Для співбесідиКоротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.