こんにちは!TypeScriptの世界へようこそ。フルスタックアーキテクトの私です。
他の言語(例えばC#やJava、あるいは素のJavaScriptなど)からTypeScriptに入ってきたとき、誰もが一度は「あれ、型定義って同じようなことを何度も書かないといけないの?」という小さな面倒臭さにぶつかるものです。
例えば、ユーザーの権限リストや、画面に表示するステータスの選択肢を配列で定義したとします。その後、その配列に新しい項目を追加したり変更したりしたとき、それに対応する型定義(`type`や`interface`)までわざわざ手動で書き直していませんか?
実はそれ、TypeScriptのIndexed Access Types(インデックスアクセス型)を使えば、すべて自動化できるんです。ここをクリアすれば、あなたの書くコードは一気にDRY(Don’t Repeat Yourself)になり、TypeScriptの基本はバッチリマスターできますよ!
今日は、配列から動的に要素の型を抜き出す魔法のようなテクニックを、優しく紐解いていきましょう。
—
1. なぜ「型の二重管理」は悪なのか?
まずは、よくある「ちょっと残念なコード」から見てみましょう。
アプリケーションで、タスクのステータスを表す配列を定義するとします。
// タスクのステータス一覧
const TASK_STATUSES = [‘todo’, ‘in_progress’, ‘done’] as const;
// ステータスの型を手動で定義しちゃった例
type TaskStatus = ‘todo’ | ‘in_progress’ | ‘done’;
一見、何の問題もないように見えますよね。でも、プロジェクトが進んで「やっぱり『レビュー中 (review)』っていうステータスを追加しよう!」となったときどうなるでしょうか?
1. `TASK_STATUSES` 配列に `’review’` を追加する。
2. 忘れずに `type TaskStatus` にも `’review’` を追加する。
人間は忘れる生き物です。もし2番目の手順を忘れたらどうなるか?コードベースのあちこチで型エラーが起きてしまいます。「配列を変えたんだから、型も連動して勝手に変わってくれよ!」と思いますよね。
その願い、TypeScriptなら一瞬で叶います。
—
2. 解決の鍵:`typeof` と 配列のインデックスアクセス
TypeScriptには、「値から型を抽出する機能(`typeof`)」と、「型の中から特定の要素を取り出す機能(Indexed Access Types)」が備わっています。
まずはイメージ図で、この2つがどう連携するのか頭に入 `= 脳内トレース` しましょう。
[値の世界]
const TASK_STATUSES = [‘todo’, ‘in_progress’, ‘done’] as const;
│
▼ (typeof で「値」を「型」に変換)
[型の世界]
readonly [“todo”, “in_progress”, “done”]
│
▼ ([number] で「配列全体の要素の型」を一網打尽に抽出!)
“todo” | “in_progress” | “done”
では、実際にコードで書いてみます。
// 1. as const をつけて「変更不可能なタプル(readonly配列)」にする
const TASK_STATUSES = [‘todo’, ‘in_progress’, ‘done’] as const;
// 2. 配列の型から、要素の型を動的に抽出する!
type TaskStatus = typeof TASK_STATUSES[number];
// 【結果】
// TaskStatus は自動的に “todo” | “in_progress” | “done” になります!
たったこれだけです!
ここで登場した `typeof TASK_STATUSES[number]` という書き方が、今回最大の主役である Indexed Access Types(インデックスアクセス型) です。
JavaScriptで配列の要素にアクセスするとき、`array[0]` や `array[index]` と書きますよね。それと全く同じ要領で、「型に対して `[number]` というインデックスを指定する」ことで、配列やタプルに含まれるすべての要素の型をユニオン型(`A | B | C`)として一気に吸い出すことができるのです。
—
3. もう一つの強力な武器:タプル型からの特定要素の抽出
配列だけでなく、要素の順番や個数がカッチリ決まった「タプル型」に対しても、このインデックスアクセスは絶大な効果を発揮します。
例えば、APIから「ユーザーID(数値)」と「ユーザー名(文字列)」がペアになった配列が返ってくることが分かっているとします。
// [id, name] のペアを表すタプル
type UserEntry = [id: number, name: string];
// 0番目の要素(id)の型だけが欲しい!
type UserId = UserEntry[0]; // ➔ number 型になる!
// 1番目の要素(name)の型だけが欲しい!
type UserName = UserEntry[1]; // ➔ string 型になる!
配列全体ではなく、「決まった位置の型だけをピンポイントで引っこ抜きたい」というときにも、このインデックスアクセス型は非常に役立ちます。関数の引数の型を再利用したいときなどにも応用できる、極めて実用性の高いテクニックです。
—
4. 陥りやすい文法エラーと注意点
ここで、初心者の開発者が非常につまずきやすいポイントをいくつか先回りして解説しておきますね。
注意点1:`as const`(constアサーション)を忘れない
先ほどのコードで、`as const` をつけたのを覚えていますか?もしこれをつけ忘れると、型はどう評価されるでしょうか。
// ❌ as const をつけ忘れた場合
const TASK_STATUSES = [‘todo’, ‘in_progress’, ‘done’];
type TaskStatus = typeof TASK_STATUSES[number];
// おっと! TaskStatus は “todo” | “in_progress” | “done” ではなく、
// 単なる string 型になってしまいます!
理由:
`as const` がないと、TypeScriptは「この配列の要素は、あとから書き換えられるかもしれない(例:`TASK_STATUSES[0] = ‘other’`)」と解釈します。そのため、要素の型はリテラル型(`’todo’`など)ではなく、より広いプリミティブ型である `string` として推論されてしまうのです。
配列の要素を定数として型に落とし込みたいときは、必ず `as const` をセットで使うと覚えておいてください。
—
5. 実務で活きる!DRYなコンポーネント設計への応用
この手法が現場でどう活きるか、もう少し実践的な例を見てみましょう。
例えば、Reactなどのフロントエンドで、セレクトボックスの選択肢を作る場面を想像してください。
// 選択肢のマスターデータ
const THEMES = [
{ id: ‘light’, label: ‘ライトモード’ },
{ id: ‘dark’, label: ‘ダークモード’ },
{ id: ‘system’, label: ‘システム設定に従う’ },
] as const;
// 1. 選択肢のオブジェクト全体の型から、個々のオブジェクトの型を抽出
type ThemeOption = typeof THEMES[number];
// 結果: { readonly id: “light”; readonly label: “ライトモード”; } | …
// 2. さらに id の値だけを抽出して、有効なテーマIDの型を作る!
type ThemeId = ThemeOption[‘id’];
// 結果: “light” | “dark” | “system”
これの何が素晴らしいかと言うと、将来的にデザイナーから「セピアモード(`sepia`)を追加してほしい」と言われたとき、`THEMES` 配列に1行追加するだけで、`ThemeOption` も `ThemeId` も、それを利用しているすべてのコンポーネントの型チェックも、自動的にすべて最新の状態に同期される点です。
手動で型を書き直す必要は、もう二度とありません。
—
まとめ
いかがでしたか?
- `typeof` で値の世界から型の世界へブリッジする
- Indexed Access Types (`[number]` や `[0]`) で、配列やタプルから必要な型を動的に引っ張り出す
- 配列を定義するときは `as const` を忘れない
この3つをマスターするだけで、あなたのTypeScriptコードは冗長な型定義から解放され、非常に保守性の高い「生きたコード」に生まれ変わります。
最初は少し呪文のように見えるかもしれませんが、コンパイラが裏側でどう型を評価しているかをイメージできるようになると、TypeScriptを書くのが何倍も楽しくなりますよ。ぜひ、今日のコードをご自身のプロジェクトでも試してみてくださいね!