お疲れ。最近、君の書いているコードをいくつかレビューさせてもらったんだけどさ……うん、全体的にすごくよく書けている。キャッチアップも早いし、TypeScriptの型システムに対する苦手意識もなさそうだ。
ただ、ふと気になったんだよね。オブジェクトの型定義をするとき、何でもかんでもインデックスシグネチャ(`{ [key: string]: any }` みたいなやつね)で適当に済ませていたり、逆にガチガチのインターフェースを無駄に量産してないかい?
中級からもう一歩抜け出して「頼れるシニア」になるためには、TypeScriptのユーティリティ型、特に `Record
今回は、この `Record
—
そもそも `Record` とは何か?(おさらいと本質)
公式ドキュメントを引くまでもないかもしれないが、`Record
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
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
罠2: `Record` という「TypeScriptを諦めた型」の多用
`Record
どうしようもなく動的なオブジェクトを扱う場合でも、せめて `Record
—
まとめ
`Record
「キーの集合を静的にコントロールし、コードの意図を明確にするための強力な契約書」なんだ。
明日から自分のコードを書くとき、あるいはチームメンバーのPRをレビューするときに、
「ここ、インデックスシグネチャになってるけど `Record` でキーを絞り込めるんじゃないか?」
という視点を持ってみてほしい。
君の書くコードが、より堅牢で、保守性の高いものに生まれ変わるはずさ。それじゃ、今日のところはここまで。また何かあったらいつでも聞きに来てくれ。

コメント