【実務・中級編】 Recordによるオブジェクト型の生成 – TypeScript実践ガイド

お疲れ。最近、君の書いているコードをいくつかレビューさせてもらったんだけどさ……うん、全体的にすごくよく書けている。キャッチアップも早いし、TypeScriptの型システムに対する苦手意識もなさそうだ。

ただ、ふと気になったんだよね。オブジェクトの型定義をするとき、何でもかんでもインデックスシグネチャ(`{ [key: string]: any }` みたいなやつね)で適当に済ませていたり、逆にガチガチのインターフェースを無駄に量産してないかい?

中級からもう一歩抜け出して「頼れるシニア」になるためには、TypeScriptのユーティリティ型、特に `Record` をどれだけ手足のように使いこなせるかが一つの分かれ道になる。

今回は、この `Record` に焦点を当てて、実務の現場でどういう思想で使い分けるべきか、ブラウザの裏側の動きまで含めて徹底的に叩き込んでやろうと思う。コーヒーでも飲みながら、じっくり聞いてくれ。

—

そもそも `Record` とは何か?(おさらいと本質)

公式ドキュメントを引くまでもないかもしれないが、`Record` は「キーの集合 `K`」と「値の型 `T`」を持つオブジェクト型を生成する組み込みのユーティリティ型だ。

TypeScriptの内部実装を覗いてみると、その正体は実にシンプル。たったこれだけのことだ。

type Record = {
[P in K]: T;
};

「なんだ、ただのマッピング型(Mapped Types)のラッパーじゃないか」と思ったかい?その通り。だが、この「シンプルさ」の裏側に、型安全性を担保するための強力な武器が隠されているんだ。

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

現場でよく見かけるアンチパターンがこれだ。

// 良くある、でもちょっと危険なインデックスシグネチャ
type UserRoles = {
[key: string]: string;
}

これの何が問題か分かるかい?
キーが `string` 全般を許容してしまうため、存在しないプロパティにアクセスしても、TypeScriptのコンパイラは「あ、存在するかもしれないね」とスルーしてしまう。つまり、タイポ(打ち間違い)を完全にスルーしやがるんだ。

一方、`Record` を使ってキーを厳密に制限すると、こうなる。

type Role = ‘admin’ | ‘editor’ | ‘viewer’;

// Role型以外のキーを絶対に許さないオブジェクト型
type Permissions = Record;

もしここで、うっかり `administrator` なんていうタイポをした日には、TypeScriptのコンパイラが「そんなキーは `Role` に存在しねぇよ!」って赤く怒ってくれる。この「静的解析によるタイポの早期発見」こそが、俺たちがTypeScriptを使う最大の理由の一つだろ?

—

ブラウザの裏側で何が起きているか?(TypeScriptの幻想とJavaScriptの現実)

ここで少し視点を変えて、TypeScriptがコンパイルされた後、ブラウザのJavaScriptエンジン(V8など)の裏側で何が起きているのかを話しておこう。

僕たちは普段、`Record` やユニオン型を使って、まるで厳格なスキーマがあるかのようにコードを書く。しかし、忘れてはならないのは、ブラウザが実行するのはただのプレーンなJavaScript(JSON的なハッシュマップ)であるという現実だ。

1. コンパイル時の幻想: TypeScriptは `Record<'apple' | 'banana', number>` を見て、「このオブジェクトには必ず `apple` と `banana` のキーが存在し、値は `number` である」と静的に保証する。
2. 実行時の現実: ブラウザのメモリ上では、単なる `Object`(または `Map`)のハッシュ構造に過ぎない。V8エンジンは、プロパティの隠しクラス(Hidden Class / Shapes)を最適化しようとするが、動的にキーが追加・削除されるとインラインキャッシュが効かなくなる。

だからこそ、「型定義でキーの網羅性を担保しつつ、ランタイムでは余計なプロパティを持たせない」という設計が、フロントエンドのパフォーマンスとメモリ効率の観点からも非常に重要になってくるんだ。

—

【実務で即コピペ可能】現場のユースケース別・実践パターン

前置きはこのくらいにして、明日からそのままプロダクトコードに組み込める実践的なパターンをいくつか紹介しよう。

パターン1: フォームの状態管理とバリデーションエラーの型定義

実務で一番頭を悩ませるのが、フォームの入力値管理と、それに紐づくエラーメッセージの管理だ。ここで `Record` が神がかった働きをする。

// 1. フォームのフィールド名をユニオン型で定義
type ProfileField = ‘username’ | ‘email’ | ‘age’;

// 2. 入力値の型(すべてstringとして扱う例)
type ProfileFormValues = Record;

// 3. エラーメッセージの型
// Partialを使うことで、「エラーが起きていないフィールドはプロパティ自体が存在しない」状態を作れる
type ProfileFormErrors = Partial>;

// — 実装例 —
const formValues: ProfileFormValues = {
username: ‘Taro Yamada’,
email: ‘taro@example.com’,
age: ’28’,
// ここでフィールドを削ったり、存在しないキーを入れると即座に型エラーになる
};

const formErrors: ProfileFormErrors = {
email: ‘有効なメールアドレスを入力してください’,
// usernameやageにはエラーがないのでプロパティすら書かなくてOK
};

パターン2: APIレスポンスのマッピングと状態別コンポーネントのディスパッチ

デザインシステムやUIコンポーネントのステータス(loading, success, error など)に応じて、表示する文言やアイコンをマッピングする辞書構造を作る際にも `Record` は鉄板だ。

type FetchStatus = ‘idle’ | ‘loading’ | ‘succeeded’ | ‘failed’;

// 各ステータスに応じた設定オブジェクトを強制する
// キーの網羅性(Exhaustiveness)をTypeScriptに強制させられるのが強み
const statusConfig: Record = {
idle: { label: ‘未取得’, badgeColor: ‘gray’ },
loading: { label: ‘読み込み中…’, badgeColor: ‘blue’ },
succeeded: { label: ‘取得完了’, badgeColor: ‘green’ },
failed: { label: ‘エラー発生’, badgeColor: ‘red’ },
};

// もしここで FetchStatus に ‘retry’ が追加されたのに
// statusConfig に定義し忘れると、TSが「おい、足りないぞ」と教えてくれる

—

シニアが教える! `Record` を使うときの罠とアンチパターン

最後に、現場でやりがちな「やっちまった」事例を共有しておく。これを知っておくだけで、無駄なバグやリファクタリングの苦しみから解放されるはずだ。

罠1: 動的にキーが増えるものに厳格な `Record` を使ってしまう

ユーザーが自由に入力できるタグのリストや、バックエンドから何が飛んでくるか分からないフリーフォームのメタデータを `Record` で縛ろうとすると、型エラーの嵐に溺れることになる。
こういうときは、素直にインデックスシグネチャや `Record` を使って、境界線(Boundary)で型ガード(Type Guard)を通すアプローチをとるべきだ。

罠2: `Record` という「TypeScriptを諦めた型」の多用

`Record` は、TypeScriptにおける `any` と何ら変わらない。これを使った瞬間から、型安全性の保護壁は崩壊し、ただのJavaScriptに逆戻りだ。
どうしようもなく動的なオブジェクトを扱う場合でも、せめて `Record` にして、使う手前で `typeof` や Zod などのバリデーションライブラリで型を絞り込む(Narrowing)癖をつけよう。

—

まとめ

`Record` は、単なる「便利な辞書型の構文」じゃない。
「キーの集合を静的にコントロールし、コードの意図を明確にするための強力な契約書」なんだ。

明日から自分のコードを書くとき、あるいはチームメンバーのPRをレビューするときに、
「ここ、インデックスシグネチャになってるけど `Record` でキーを絞り込めるんじゃないか?」
という視点を持ってみてほしい。

君の書くコードが、より堅牢で、保守性の高いものに生まれ変わるはずさ。それじゃ、今日のところはここまで。また何かあったらいつでも聞きに来てくれ。

コメント

タイトルとURLをコピーしました