Record を使うべき場面、使わないほうがいい場面

Record<K, V> はオブジェクトの型を定義するユーティリティ型だ。便利だが、何でもかんでも Record で書けばいいわけではない。適切な場面と避けるべき場面を整理する。

Record<K, V> とは

Record<K, V> は「キーが K 型、値が V 型であるオブジェクト」を表す。

type UserRoles = Record<string, boolean>;

const roles: UserRoles = {
  admin: true,
  editor: false,
  viewer: true,
};

内部的には以下のように定義されている。

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

マップ型(Mapped Types)を使って、K のすべてのキーに対して T 型の値を持つオブジェクトを生成する。

使うべき場面

辞書・マップ構造

キーが動的に決まるオブジェクトには Record が適している。

// ユーザーIDをキーとした辞書
type UserMap = Record<string, User>;

const users: UserMap = {
  "u001": { name: "Alice", age: 25 },
  "u002": { name: "Bob", age: 30 },
};

キーを限定したい場合

ユニオン型と組み合わせると、特定のキーだけを許可できる。

type Theme = "light" | "dark" | "system";
type ThemeColors = Record<Theme, string>;

const colors: ThemeColors = {
  light: "#ffffff",
  dark: "#000000",
  system: "#808080",
};

すべてのキーを網羅しないとエラーになるため、漏れを防げる。

const colors: ThemeColors = {
  light: "#ffffff",
  dark: "#000000",
  // エラー: Property 'system' is missing
};
辞書・マップ構造

キーが動的に決まるデータ構造。ユーザー ID、設定キーなど。

網羅性を保証したいオブジェクト

ユニオン型のすべての値に対応するオブジェクト。テーマ、ステータス、言語コードなど。

使わないほうがいい場面

固定のプロパティを持つオブジェクト

プロパティが決まっているなら、普通のオブジェクト型を使うべきだ。

// 悪い例
type User = Record<"name" | "email" | "age", string>;

// 良い例
type User = {
  name: string;
  email: string;
  age: number;  // 型を個別に指定できる
};

Record だとすべての値が同じ型になってしまう。プロパティごとに型が異なる場合は使えない。

省略可能なプロパティがある場合

Record はすべてのキーを必須にする。省略可能なプロパティを表現できない。

// これはできない
type Config = Record<"host" | "port" | "timeout", string | undefined>;
// timeout を省略することはできない

// こうすべき
type Config = {
  host: string;
  port: string;
  timeout?: string;
};
Record

すべてのキーが必須。すべての値が同じ型。動的なキーに適する。

オブジェクト型

プロパティごとに型を変えられる。省略可能なプロパティを定義できる。固定構造に適する。

Partial<Record<K, V>> という選択肢

すべてのキーを省略可能にしたい場合、Partial と組み合わせる。

type Theme = "light" | "dark" | "system";
type PartialThemeColors = Partial<Record<Theme, string>>;

const colors: PartialThemeColors = {
  light: "#ffffff",
  // dark と system は省略可能
};

ただし、これなら最初から { [key in Theme]?: string } と書いたほうが読みやすいこともある。

インデックスシグネチャとの違い

Record<string, T>{ [key: string]: T } とほぼ同じだが、微妙な違いがある。

type A = Record<string, number>;
type B = { [key: string]: number };

// A と B はほぼ同等だが、
// Record のほうが意図が明確

Record を使うと「これは辞書構造である」という意図が伝わりやすい。一方、インデックスシグネチャは他のプロパティと混在できる柔軟性がある。

type Mixed = {
  id: string;  // 固定プロパティ
  [key: string]: string;  // 動的プロパティ
};

結論として、Record は辞書構造やユニオン型の網羅に使い、固定構造のオブジェクトには普通の型定義を使おう。適材適所で使い分けることが大切だ。