Skip to main content

Утилітарний тип record у TypeScript

Record<K, T> - це utility-тип у TypeScript, який створює тип об'єкта, де кожен ключ з union K є обов'язковим і відповідає типу T.

Теорія

TL;DR

  • Аналогія: Record - це шаблон таблиці, де заголовки стовпців фіксовані, а кожна клітинка містить дані одного типу.
  • Головна різниця від { [key: string]: T }: Record вимагає ВСІ ключі з union в об'єкті; index-сигнатури приймають будь-який рядок без перевірок.
  • Всередині Record<K, T> розгортається у { [P in K]: T } - це mapped-тип.
  • Правило вибору: Record - якщо ключі відомі наперед. Index-сигнатура або Map - якщо ключі динамічні.

Швидкий приклад

typescript
type Status = "pending" | "approved" | "rejected"; type StatusConfig = Record<Status, { label: string; color: string }>; const config: StatusConfig = { pending: { label: "Очікує", color: "#FFA500" }, approved: { label: "Виконано", color: "#00AA00" }, rejected: { label: "Відхилено", color: "#FF0000" } // Прибери будь-який ключ - TypeScript одразу покаже помилку. };

Усі три ключі обов'язкові. В цьому весь контракт.

Ключова різниця

Record гарантує повноту покриття. Якщо оголосити Record<"a" | "b" | "c", T>, TypeScript вимагає всі три ключі під час компіляції. Index-сигнатура { [key: string]: T } приймає будь-який рядок без перевірок. Record робить контракти явними. Index-сигнатури дають гнучкість.

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

  • Конфіги з відомими ключами: ролі до дозволів, статус-коди до повідомлень, feature-флаги до boolean.
  • Lookup-таблиці: ID користувачів до профілів, коди валют до курсів.
  • Стейт-машини: кожен стан відображається на дозволені переходи або обробники.
  • Не використовуй Record, якщо ключі справді динамічні або необмежені. Там краще підходить index-сигнатура або Map.

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

TypeScript розгортає Record<K, T> у { [P in K]: T }. Компілятор перебирає кожен елемент union K і створює обов'язкову властивість. Якщо якийсь ключ відсутній - помилка компіляції. В рантаймі накладних витрат немає. Record компілюється у звичайний JavaScript-об'єкт.

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

Помилка 1: Забутий ключ після розширення union

Найчастіше бачу це після рефакторингу: хтось додає нове значення до union і не оновлює об'єкт.

typescript
type Permission = "read" | "write" | "delete" | "admin"; type PermissionConfig = Record<Permission, boolean>; // Помилка TypeScript: властивість 'admin' відсутня const config: PermissionConfig = { read: true, write: true, delete: true, }; // Виправлення: додай усі ключі const configFixed: PermissionConfig = { read: true, write: true, delete: true, admin: false, };

Помилка 2: Record там, де потрібні опціональні ключі

typescript
type Feature = "darkMode" | "analytics" | "beta"; // Record вимагає всі ключі, тому це не спрацює: const flags: Record<Feature, boolean> = { darkMode: true, analytics: true, // Помилка: відсутній "beta" }; // Виправлення: загорни у Partial const flagsFixed: Partial<Record<Feature, boolean>> = { darkMode: true, analytics: true, // Тепер валідно };

Помилка 3: Record<string, T> замість конкретного union

typescript
// Це просто index-сигнатура під іншою назвою type D = Record<string, number>; // Те саме, що { [key: string]: number } // Якщо ключі відомі - перелічи їх явно type Currency = "USD" | "EUR" | "GBP"; const rates: Record<Currency, number> = { USD: 1.0, EUR: 0.92, GBP: 0.87, // TypeScript вимагає всі три };

Де зустрічається в реальних проєктах

  • React: реєстри компонентів Record<ComponentName, ComponentType>
  • Redux: маппінг action-типів до обробників Record<ActionType, ActionCreator>
  • Express: HTTP-метод до обробника Record<"GET" | "POST" | "DELETE", RequestHandler>
  • Тести: фабрики моків Record<UserRole, MockUser>

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

Q: У чому різниця між Record<K, T> і { [P in K]: T }?
A: Вони еквівалентні. Record - це синтаксичний цукор над mapped-типом. Використовуй Record для простих контрактів ключ-значення. Mapped-тип пиши напряму, якщо потрібна умовна логіка для кожного ключа: { [P in K]: P extends "admin" ? AdminConfig : UserConfig }.

Q: Як зробити частину ключів Record опціональними?
A: Partial<Record<K, T>> робить усі ключі опціональними. Якщо хочеш зберегти всі ключі обов'язковими, але дозволити undefined, використовуй Record<K, T | undefined>.

Q: Коли обрати Map<string, T> замість Record<string, T>?
A: Record підходить для статичних типізованих конфігурацій, які компілюються у звичайні об'єкти. Map - для динамічних даних з необмеженими ключами і вбудованими методами ітерації. Конфіги - Record. Кеші або дані від користувача - Map.

Q: Що станеться, якщо додати зайвий ключ у рантаймі?
A: JavaScript дозволяє. Record перевіряє контракт лише під час компіляції. TypeScript покаже помилку типу для config.newKey = "value", але присвоєння виконається.

Приклади

Маппінг дозволів за ролями

typescript
type UserRoles = "admin" | "user" | "guest"; type RolePermissions = Record<UserRoles, string[]>; const rolePermissions: RolePermissions = { admin: ["create", "edit", "delete"], user: ["view", "edit"], guest: ["view"], }; // TypeScript знає: це string[], а не string[] | undefined console.log(rolePermissions["admin"]); // ["create", "edit", "delete"]

Кожен ключ з UserRoles обов'язковий. Додай "moderator" до union - TypeScript одразу вимагатиме властивість moderator в об'єкті.

Обробники HTTP-методів в Express

typescript
import { Request, Response } from "express"; type RouteHandlers = Record< "GET" | "POST" | "DELETE", (req: Request, res: Response) => void >; const handlers: RouteHandlers = { GET: (req, res) => res.json({ data: [] }), POST: (req, res) => res.status(201).json({}), DELETE: (req, res) => res.status(204).send(), };

Додай новий HTTP-метод до union - код не скомпілюється, поки не напишеш його обробник. Тип забезпечує повне покриття автоматично.

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

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

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

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