Avatar ·

Что такое Record<K, V> в TypeScript и как его использовать?

Привет! Я начал изучать TypeScript и наткнулся на тип `Record`. Честно говоря, я не до конца понимаю, для чего он нужен и чем отличается от обычного объекта или интерфейса. Допустим, у меня есть такой код: ```typescript interface User { id: number; name: string; } const users: { [key: number]: User } = { 1: { id: 1, name: 'Анна' }, 2: { id: 2, name: 'Борис' } }; ``` Я вижу, что можно написать `{ [key: number]: User }`. Но в документации я встретил `Record`. Это одно и то же? Или есть нюансы? Также я пытался создать объект, где ключи — это строки, которые могут быть только определёнными значениями, например, статусами заказа: 'pending', 'shipped', 'delivered'. Я хочу, чтобы ключи были строго из этого набора, а значения — датой (строкой). Как это сделать с помощью Record? И можно ли вообще? Буду благодарен за понятное объяснение с примерами, потому что в интернете много сложных статей, а мне нужно простое и рабочее объяснение для новичка. <<>>

Что такое 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 с перечислением всех ключей вручную.