Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Як працюють keyof та typeof у TypeScript». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**keyof** витягує імена властивостей типу як union рядкових літералів. **typeof** фіксує тип будь-якого значення під час компіляції. ```typescript const user = { name: "Bob", age: 30 }; type User = typeof user; // { name: string; age: number } type UserKey = keyof typeof user; // "name" | "age" ``` **Ключове:** `keyof` працює з типами і повертає union ключів; `typeof` працює зі значеннями і повертає їх тип. Разом вони дозволяють отримати union ключів прямо з рантайм-значення.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**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]` ### Порівняння | | `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` на примітивному типі** ```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]>` витягує список аргументів для конкретного обробника. І назва події, і її аргументи перевіряються на рівні типів ще до запуску коду.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.