Record в TypeScript: разбираемся на практике
Представьте, что вы пишете приложение для изучения английских слов. Вам нужно хранить переводы: cat → кошка, dog → собака. Как это правильно затипизировать в TypeScript? Можно создать интерфейс, но что, если ключи заранее неизвестны? Или их слишком много? Тут на помощь приходит Record — один из самых полезных, но часто недооценённых утилитарных типов.
В этой статье мы разберём, что такое Record type в TypeScript, на реальных примерах, которые вы сможете сразу применить в своих проектах. Никакой скучной теории — только практика.
Что такое Record в TypeScript? Простыми словами
Record<K, V> — это встроенный утилитарный тип, который создаёт объект, где все ключи имеют тип K, а все значения — тип V.
Думайте о нём как о типизированном словаре или карте с фиксированным набором ключей.
Базовая запись выглядит так:
// Обычный объект
const userRoles: Record<string, string> = {
admin: 'Администратор',
user: 'Пользователь',
guest: 'Гость'
};
Здесь Record<string, string> означает: «Объект, где ключи — строки, и значения — тоже строки». Просто и понятно.
Чем Record лучше обычного интерфейса?
- Гибкость ключей: Вы можете задать конкретный набор ключей через union-тип.
- Лаконичность: Не нужно описывать каждое поле вручную.
- Безопасность: TypeScript проверит, что вы используете только разрешённые ключи.
Record vs простой объект: наглядный пример
Допустим, у нас есть статусы заказа: pending, shipped, delivered. Без Record пришлось бы писать интерфейс с тремя полями:
interface OrderStatusMap {
pending: string;
shipped: string;
delivered: string;
}
const statusDescriptions: OrderStatusMap = {
pending: 'Ожидает обработки',
shipped: 'Отправлен',
delivered: 'Доставлен'
};
А с Record — компактнее и нагляднее:
type OrderStatus = 'pending' | 'shipped' | 'delivered';
const statusDescriptions: Record<OrderStatus, string> = {
pending: 'Ожидает обработки',
shipped: 'Отправлен',
delivered: 'Доставлен'
};
Плюс: если вы попытаетесь добавить cancelled в объект, TypeScript сразу укажет на ошибку — такого статуса нет в union-типе.
Топ-5 практических примеров использования Record
Давайте посмотрим, где Record реально упрощает жизнь.
1. Конфигурация приложения
Храните настройки с известными ключами, но разными типами значений:
type ConfigKeys = 'apiUrl' | 'timeout' | 'retryCount';
type ConfigValue = string | number | boolean;
const appConfig: Record<ConfigKeys, ConfigValue> = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retryCount: 3
};
2. Маппинг enum или констант
Часто нужно сопоставить код и его человекочитаемое название:
enum HttpStatus {
OK = 200,
NotFound = 404,
Error = 500
}
const statusMessages: Record<HttpStatus, string> = {
[HttpStatus.OK]: 'Всё хорошо',
[HttpStatus.NotFound]: 'Не найдено',
[HttpStatus.Error]: 'Ошибка сервера'
};
3. Формы и валидация
Record отлично подходит для хранения ошибок валидации по полям:
type FormFields = 'email' | 'password' | 'name';
type ValidationErrors = Record<FormFields, string | null>;
const errors: ValidationErrors = {
email: 'Некорректный email',
password: null,
name: 'Имя обязательно'
};
4. Кэш данных
Имитация кэша с ключами-идентификаторами:
type UserId = number;
type UserCache = Record<UserId, { name: string; age: number }>;
const cache: UserCache = {
1: { name: 'Анна', age: 25 },
2: { name: 'Иван', age: 30 }
};
5. Динамические маршруты в React-приложении
Record помогает типизировать пропсы:
type RouteParams = Record<string, string>;
function RouteComponent({ params }: { params: RouteParams }) {
return <div>Параметр: {params.id}</div>;
}
Типизированные ключи: Record с union-типом
Самая мощная фича Record — использование union-типа в качестве ключей. Это даёт строгую проверку на этапе компиляции.
type Fruit = 'apple' | 'banana' | 'orange';
type FruitPrices = Record<Fruit, number>;
const prices: FruitPrices = {
apple: 100,
banana: 80,
orange: 90
};
// Ошибка! Свойства "grape" нет в типе Fruit
// prices.grape = 120;
Если вам нужно сделать все поля необязательными — комбинируйте с Partial:
type PartialFruitPrices = Partial<Record<Fruit, number>>;
// { apple?: number; banana?: number; orange?: number; }
Record с вложенными объектами
Record может содержать другие объекты или даже другие Record:
type UserProfile = {
name: string;
age: number;
email: string;
};
type UsersDatabase = Record<string, UserProfile>;
const users: UsersDatabase = {
'user_1': { name: 'Алексей', age: 28, email: 'alex@example.com' },
'user_2': { name: 'Мария', age: 32, email: 'maria@example.com' }
};
Частые ошибки новичков с Record в TypeScript
Вот три типичные проблемы, с которыми сталкиваются начинающие:
Ошибка 1: Несоответствие типов ключа и значения
// Ошибка: число нельзя использовать как ключ, если K — string
const bad: Record<string, number> = { 1: 'one' }; // ❌
Решение: Убедитесь, что тип ключа поддерживает значения, которые вы передаёте.
Ошибка 2: Забыли указать все ключи из union-типа
type Status = 'active' | 'inactive';
const map: Record<Status, string> = { active: 'Активен' }; // ❌ пропущен 'inactive'
Решение: Используйте Partial<Record<Status, string>&