Что такое Record<K, V> в TypeScript и как его использовать?
Что такое Record?
Record<K, V> — это встроенный утилитарный тип в TypeScript, который создаёт тип объекта, где все ключи имеют тип K, а все значения — тип V. По сути, это более краткая и читаемая запись индексной сигнатуры { [key: K]: V }.
Да, Record<number, User> эквивалентно { [key: number]: User }. Но у Record есть важные преимущества:
- Лаконичность — меньше кода, меньше шансов ошибиться.
- Ограничение ключей — K может быть не только примитивом, но и юнион-типом (объединением строковых литералов). Это самая мощная фича.
- Читаемость — сразу понятно: "это объект, где ключи — такие-то, значения — такие-то".
Пример 1: Простая замена индексной сигнатуры
// Вместо этого:
const users1: { [key: number]: User } = {
1: { id: 1, name: 'Анна' },
2: { id: 2, name: 'Борис' }
};
// Пишем так:
const users2: Record<number, User> = {
1: { id: 1, name: 'Анна' },
2: { id: 2, name: 'Борис' }
};
Пример 2: Строгие ключи (юнион-тип) — то, что вы хотели
Допустим, у вас есть статусы заказа. Вы хотите, чтобы в объекте были ТОЛЬКО эти ключи, и каждый ключ обязан присутствовать.
type OrderStatus = 'pending' | 'shipped' | 'delivered';
// Каждый статус должен быть ключом, значение — дата (строка)
const orderDates: Record<OrderStatus, string> = {
pending: '2025-04-01',
shipped: '2025-04-03',
delivered: '2025-04-05'
};
// Если вы забудете один статус — TypeScript выдаст ошибку:
// ❌ Property 'delivered' is missing in type...
const badDates: Record<OrderStatus, string> = {
pending: '2025-04-01',
shipped: '2025-04-03'
// забыли 'delivered'
};
Пример 3: Record с опциональными ключами (Partial)
Если не все ключи обязательны, комбинируем с Partial:
type Status = 'active' | 'inactive' | 'banned';
// Partial делает все свойства необязательными
const userFlags: Partial<Record<Status, boolean>> = {
active: true
// inactive и banned необязательны
};
Пример 4: Вложенные объекты
interface Product {
price: number;
inStock: boolean;
}
// Ключи — ID товаров (строки), значения — объекты Product
const inventory: Record<string, Product> = {
'prod-1': { price: 100, inStock: true },
'prod-2': { price: 250, inStock: false }
};
Когда НЕ использовать Record?
- Если вам нужны методы объекта (например,
toString), лучше использовать интерфейс. - Если структура объекта сложная, с разными типами для разных ключей — Record не подходит, так как он требует единый тип для всех значений.
- Если ключи — это
stringбез ограничений, и вы хотите проверять каждый ключ отдельно — лучше интерфейс.
Итог
Record<K, V> — это элегантный способ сказать TypeScript: "У меня есть объект-словарь, где все ключи одного типа, и все значения одного типа". Особенно полезен с юнион-типами для создания строгих маппингов. Используйте его вместо громоздких индексных сигнатур — код станет чище и безопаснее.
Альтернативный взгляд: Record как фабрика типов
Можно думать о Record<K, V> как о функции создания типа на уровне типов. Это особенно удобно, когда вы работаете с конфигурациями или маппингом одного множества на другое.
Например, если у вас есть enum или константы:
const COLORS = ['red', 'green', 'blue'] as const;
type Color = typeof COLORS[number]; // 'red' | 'green' | 'blue'
// Создаём тип, где каждый цвет — ключ, а значение — HEX-код
type ColorMap = Record<Color, string>;
const hexColors: ColorMap = {
red: '#FF0000',
green: '#00FF00',
blue: '#0000FF'
};
Важный нюанс: Record не гарантирует, что ключи будут именно теми, что вы указали, если K — это string. Он гарантирует это только если K — юнион-тип с конкретными литералами. В случае с Record<string, V> вы можете использовать любую строку в качестве ключа — это обычный словарь.
Также помните, что Record — это не магический тип. Он просто создаёт объект с индексной сигнатурой. Если вам нужно что-то более сложное (например, разные типы для разных ключей), используйте интерфейс или type с перечислением всех ключей вручную.