こんにちは!TypeScriptの世界へようこそ。
TypeScriptを学び始めて、「型を定義するのって、なんだか同じようなコードを何度も書いている気がするな……」と感じたことはありませんか?
例えば、あるオブジェクトの型をベースにして「すべてのプロパティを読み取り専用(readonly)にした型」や「すべてをオプション(?)にした型」を、手作業でコピペして作っているとしたら……それは非常にもったいないことです!
TypeScriptには、既存の型をベースに、まるでプログラミングのループ処理のように動的に新しい型を作り出す「Mapped Types(マッピングされた型)」という強力な仕組みがあります。
「難しそう……」と思うかもしれませんが、大丈夫です。この記事を読み終える頃には、コンパイラが裏側でどうやって型を組み立てているのかが手に取るようにわかるようになります。「ここをクリアすれば、TypeScriptの基本はバッチリマスターできますよ」と言える重要なステップですので、一緒に楽しく学んでいきましょう!
—
1. Mapped Typesの正体:型システムにおける「for…inループ」
まずは、頭の中のイメージを整理しましょう。
JavaScript(あるいは他の言語)で、オブジェクトのキーを1つずつ取り出して処理するとき、`for…in` ループを使いますよね。
// JavaScriptでの値のループ処理
for (const key in user) {
console.log(key); // “id”, “name”, “email” が順番に取り出される
}
Mapped Typesは、これと全く同じことを「型(Type)の世界」で行うための仕組みです。
与えられた「キーのリスト」をコンパイラが1つずつループ(走査)し、新しいオブジェクトの型を自動的に組み立ててくれます。
イメージ図にすると、以下のような処理がコンパイル時に行われています。
【入力: キーの集合】 “id” | “name” | “email”
↓
【Mapped Typesのループ】(コンパイラが1つずつ取り出す)
1周目: “id” –> [ “id” ]: string
2周目: “name” –> [ “name” ]: string
3周目: “email” –> [ “email” ]: string
↓
【出力: 新しい型】 { id: string; name: string; email: string; }
手作業で新しい型を書き直す(ボイラープレートを書く)必要はありません。元の型さえあれば、TypeScriptが自動でこのループを回して、一瞬で新しい型を生成してくれるのです。
—
2. 基本構文を徹底解剖:脳内でコンパイラを動かそう
では、実際の構文を見てみましょう。Mapped Typesの基本形は以下のような形をしています。
type MyMappedType = {
[K in Keys]: NewType;
};
この短いコードの中に、重要なパーツがギュッと詰まっています。1つずつ紐解いていきましょう。
各パーツの役割
1. `Keys`(ループの対象):
ここに「プロパティ名(キー)の候補」を Union型(`”a” | “b” | “c”` のような縦棒で繋がれた型) で渡します。
2. `in`(ループの宣言):
JavaScriptの `for…in` の `in` と同じです。「右側の `Keys` から、1つずつ要素を取り出すよ」という宣言です。
3. `K`(ループ変数):
取り出された「現在のキー」が一時的に代入される変数名です(`K` や `P` がよく使われます)。
4. `NewType`(値の型):
ループで生成される各プロパティに、最終的にどのような型を割り当てるかを指定します。
具体的なコードで見てみよう
例えば、`”fever” | “cough” | “soreThroat”`(発熱、咳、喉の痛み)という症状のキーに対して、すべて `boolean`(真偽値)の型を持つオブジェクトを作ってみましょう。
// 1. キーの集合(Union型)を定義する
type Symptoms = “fever” | “cough” | “soreThroat”;
// 2. Mapped Typesを使って、各キーに対応する値を boolean にした型を動的に作る
type SymptomChecklist = {
[K in Symptoms]: boolean;
};
// — コンパイラが脳内で評価した結果、以下の型が自動生成されます —
// type SymptomChecklist = {
// fever: boolean;
// cough: boolean;
// soreThroat: boolean;
// }
// 3. 実際に使ってみる
const patientStatus: SymptomChecklist = {
fever: true,
cough: false,
soreThroat: true,
};
どうでしょうか? 自分で3つのプロパティを手書きする代わりに、TypeScriptがループを回して型を作ってくれましたね!
—
3. 陥りやすい文法エラーと対策
Mapped Typesを書き始めた人が、最初によく引っかかる「2大エラー」とその解決策を紹介します。
エラー①:`in` の代わりに `:` を使ってしまう
オブジェクトの通常の型定義に慣れていると、ついつい以下のように書いてしまいがちです。
// ❌ 間違い(コンパイルエラーになります)
type BadType = {
[K: Symptoms]: boolean; // コロン「:」を使ってしまっている
};
- なぜエラーになるの?: `:` は「この変数(またはキー)はこういう型ですよ」という一対一の指定です。ループ処理を行いたい場合は、必ず `in` を使う必要があります。
- 対策: 「型をループさせる時は `in`!」と覚えましょう。
エラー②:`in` の右側にUnion型以外(オブジェクト型など)を直接渡してしまう
これも非常によくあるミスです。
type User = {
id: string;
name: string;
};
// ❌ 間違い(コンパイルエラーになります)
type BadUserMap = {
[K in User]: boolean; // Userはオブジェクト型なので、直接ループに回せない!
};
- なぜエラーになるの?: `in` の右側には、キーの候補(`”id” | “name”` のような文字列のUnion型)が来る必要があります。`User` というオブジェクト型そのものを渡しても、コンパイラは「どのキーをループすればいいの?」と迷子になってしまいます。
- 対策: オブジェクトの「キーの集合」を取り出してくれる `keyof` 演算子 を組み合わせます。
// 正しい書き方
type CorrectUserMap = {
[K in keyof User]: boolean; // keyof User は “id” | “name” に展開されるためOK!
};
—
4. 実践:ボイラープレートを消し去るマジック
ここからは、実務で本当によく使う「既存の型をトランスフォーム(変換)する」実践的なテクニックを見ていきましょう。
例えば、以下のようなシステム開発でよくある「ユーザー情報」の型があるとします。
interface User {
id: string;
name: string;
age: number;
}
ユースケース1:すべてのプロパティを「読み取り専用」にしたい!
画面表示用のデータなど、後から書き換えられたくない「読み取り専用の型」を新しく作りたいとき、手書きで `readonly` を付け直すのは面倒ですよね。
Mapped Typesを使えば、以下のように一撃で作成できます。
// 元の型(T)を受け取って、すべてのプロパティに readonly を付与する汎用的な型
type MakeReadonly
readonly [K in keyof T]: T[K]; // T[K] は、元の型のそのプロパティの型(stringやnumberなど)をそのまま引き継ぐという意味です
};
// User型を読み取り専用に変換!
type ReadonlyUser = MakeReadonly
// — コンパイラによる評価結果 —
// type ReadonlyUser = {
// readonly id: string;
// readonly name: string;
// readonly age: number;
// }
const user: ReadonlyUser = {
id: “001”,
name: “Alice”,
age: 25
};
// ❌ エラー:readonlyなので再代入できません!
// user.name = “Bob”;
ユースケース2:すべてのプロパティを「オプション(任意)」にしたい!
「ユーザー情報の更新API」などを作る際、一部のプロパティだけを部分的に送信できるように、すべてのプロパティに `?` をつけたい場合があります。
これもMapped Typesの得意分野です。
// すべてのプロパティをオプションにする型
type MakeOptional
[K in keyof T]?: T[K]; // キー(K)のあとに「?」をつけるだけ!
};
type PartialUser = MakeOptional
// — コンパイラによる評価結果 —
// type PartialUser = {
// id?: string;
// name?: string;
// age?: number;
// }
// 一部のプロパティだけでもOK!
const updatePayload: PartialUser = {
name: “New Name”
};
実を言うと、TypeScriptに標準で用意されている `Readonly
裏側の仕組みを知ってしまえば、「なんだ、あの便利な型も自分で作れるレベルのものだったんだ!」と自信が湧いてきませんか?
—
5. 一歩先へ:修飾子の操作(`+` や `-` の魔法)
最後に、少しだけ応用的な、しかし知っておくと周りの開発者に一目置かれる「修飾子のコントロール」についてお話しします。
Mapped Typesでは、プロパティに付ける `readonly` や `?` を「追加する」だけでなく、「削除する」こともできます。その際に使うのが `+` や `-` という記号です。
応用:すべての「オプション(?)」を強制的にはぎ取る
例えば、プロパティが任意(`?`)になっている型から、強制的に `?` を取り除いて「すべての入力を必須(Required)にする」型を作ってみましょう。
interface PendingUser {
id?: string;
name?: string;
age?: number;
}
// 「-?」と書くことで、元の型にある「?」を取り除く(マイナスする)
type Concrete
[K in keyof T]-?: T[K];
};
type ActiveUser = Concrete
// — コンパイラによる評価結果 —
// type ActiveUser = {
// id: string; // 「?」が消えて必須になった!
// name: string; // 「?」が消えて必須になった!
// age: number; // 「?」が消えて必須になった!
// }
このように、`-?` は「オプショナルを禁止する」、`-readonly` と書けば「読み取り専用を解除して書き込み可能にする」という操作が可能です(`+` は省略可能なので、単に `?` や `readonly` と書いた場合は `+?` や `+readonly` と同じ意味になります)。
—
まとめ:TypeScriptを掌握する第一歩をクリア!
お疲れ様でした!今回の要点を振り返ってみましょう。
1. Mapped Typesは型の「for…inループ」:キーの集合を走査して、新しいオブジェクト型を自動生成する。
2. 基本構造は `[K in Keys]: Type`:`in` を使ってUnion型をループさせる。
3. `keyof` と組み合わせる:既存のオブジェクト型からキーを抽出し、トランスフォームする。
4. 修飾子の追加・削除:`?` や `readonly` を追加するだけでなく、`-?` や `-readonly` で剥ぎ取ることも可能。
最初は難しく見えた特殊な記法も、コンパイラが裏側で「キーを1つずつ取り出して、新しい型にペタペタと貼り付けている姿」をイメージできるようになれば、もう怖くありません。
この Mapped Types をマスターしたあなたは、TypeScriptの型システムの核心に一歩足を踏み入れました。これからは、ライブラリの型定義を読んだときも「あ、ここで型をループさせているんだな」と直感的に理解できるようになりますよ。
焦らず、一歩ずつ。この調子でTypeScriptを楽しく掌握していきましょう!