Avatar ·

Что такое Record в TypeScript и как его правильно использовать?

Всем привет! Я недавно начал изучать TypeScript и наткнулся на встроенный тип `Record`. Прочитал документацию, но до конца не понял, в чём его суть и когда его применять. Я понимаю, что есть интерфейсы и типы, но `Record` выглядит как-то иначе. Например, я видел такой код: ```typescript const users: Record = {}; ``` Но я не понимаю, чем это отличается от простого объекта `{ [key: string]: User }`. Или от `Map`? Когда что выбирать? Моя конкретная проблема: у меня есть список статусов заказа (pending, approved, rejected), и для каждого статуса нужно хранить какой-то текст (например, цвет для отображения). Я написал так: ```typescript const statusColors = { pending: 'yellow', approved: 'green', rejected: 'red' }; ``` Но TypeScript ругается, когда я пытаюсь обратиться к `statusColors['unknown']`. Как правильно затипизировать эту мапу с помощью `Record`? И вообще, для каких сценариев `Record` подходит лучше всего? Буду благодарен за примеры из реальной практики. <<>>

Что такое Record в TypeScript?

Record<K, T> — это встроенный утилитарный тип, который создаёт объектный тип, где все ключи имеют тип K, а все значения — тип T. По сути, это синтаксический сахар над индексной сигнатурой { [key: K]: T }, но с важными отличиями.

Чем Record отличается от обычной сигнатуры?

Главное отличие — Record позволяет задать конкретное множество ключей (через union-тип), а не просто "любую строку". Это даёт строгую типизацию и автодополнение.

Рассмотрим вашу задачу со статусами заказа. Вот правильное решение:

type OrderStatus = 'pending' | 'approved' | 'rejected';

const statusColors: Record<OrderStatus, string> = {
  pending: 'yellow',
  approved: 'green',
  rejected: 'red'
};

// TypeScript теперь знает все ключи:
statusColors.pending;  // ✅ OK
statusColors['approved']; // ✅ OK
// statusColors.unknown; // ❌ Ошибка: нет такого ключа

Когда использовать Record?

  • Маппинг конечного набора ключей — как в примере со статусами, где ключи известны заранее.
  • Словари с однотипными значениями — например, конфиги, переводы (i18n), кэши.
  • Типизация ответов API — когда сервер возвращает объект с динамическими ключами, но значениями одного типа.

Record vs Index Signature

// Index signature — разрешает ЛЮБЫЕ строковые ключи
type UserMap = { [key: string]: User };
const map1: UserMap = {};
map1['anything'] = new User(); // ✅ Работает, но небезопасно

// Record с конкретным union — строгие ключи
type UserRoles = 'admin' | 'user' | 'guest';
const rolePermissions: Record<UserRoles, string[]> = {
  admin: ['create', 'delete'],
  user: ['read'],
  guest: ['read']
};
// rolePermissions['superadmin'] // ❌ Ошибка компиляции

Record vs Map

Map — это JS-коллекция из ES6, которая:

  • Поддерживает любые типы ключей (объекты, функции).
  • Имеет методы .get(), .set(), .has().
  • Сохраняет порядок вставки.

Record — это просто TypeScript-тип для обычных объектов. Используйте Record, когда:

  • Ключи — строки или числа.
  • Нужна строгая проверка на этапе компиляции.
  • Не нужны методы Map (достаточно скобочной нотации).

Продвинутые примеры

// Record с вложенными типами
type Config = Record<string, { enabled: boolean; timeout: number }>;

// Частичный Record (не все ключи обязательны)
type PartialRecord<K extends string, T> = Partial<Record<K, T>>;

// Record для enum
enum Direction {
  Up = 'UP',
  Down = 'DOWN'
}
const directionLabels: Record<Direction, string> = {
  [Direction.Up]: 'Вверх',
  [Direction.Down]: 'Вниз'
};

Итог

Record — идеальный выбор, когда вам нужен объект с известным набором ключей (union) и однотипными значениями. Он даёт безопасность типов, автодополнение в IDE и читаемый код. Для динамических ключей с неизвестным набором используйте обычную индексную сигнатуру, а для сложной логики с ключами-объектами — Map.

<<>>

Альтернативный подход: использование утилитарных типов вместе с Record

Иногда полезно комбинировать Record с другими встроенными типами для более гибкой типизации.

Partial + Record для опциональных ключей

Если не все ключи обязательны, используйте Partial<Record<K, T>>:

type Status = 'pending' | 'approved' | 'rejected';
const optionalColors: Partial<Record<Status, string>> = {
  pending: 'yellow'
  // approved и rejected могут отсутствовать
};

Readonly + Record для неизменяемых конфигов

type ColorMap = Readonly<Record<Status, string>>;
const COLORS: ColorMap = {
  pending: 'yellow',
  approved: 'green',
  rejected: 'red'
};
// COLORS.pending = 'blue'; // ❌ Ошибка: только для чтения

Record с шаблонными литералами

Можно создавать сложные ключи с помощью Template Literal Types:

type EventName = `on${Capitalize<string>}`;
type EventHandlers = Partial<Record<EventName, () => void>>;

const handlers: EventHandlers = {
  onClick: () => console.log('click'),
  onHover: () => console.log('hover')
};

Этот подход особенно полезен для типизации обработчиков событий, CSS-свойств или конфигураций с префиксами. Главное — помнить, что Record — это строительный блок, который можно комбинировать с другими утилитарными типами для достижения нужной семантики.