TypeScript には Partial や Readonly のような便利な型が用意されていますが、これらは魔法ではなく Mapped Types(マップ型) という仕組みで作られています。Mapped Types は「既存の型のプロパティを1つずつ巡回し、新しい型を自動で組み立てる」機能です。この記事では、{ [K in keyof T]: ... } という基本構文から、? や readonly の付与・除去、as によるキーの付け替えまで、自分で型を作る側の目線で初心者〜中級者向けに解説します。
目次
Mapped Types とは何か
Mapped Types(マップ型)とは、既存の型のプロパティを1つずつ取り出して変換し、新しい型を生成する仕組みです。「マップ(map)」は配列の map メソッドと同じイメージで、元になる型の各プロパティに同じ処理を適用して別の型に写し替える、と考えると分かりやすいでしょう。たとえば「すべてのプロパティを省略可能にした型」や「すべてを読み取り専用にした型」を、元の型から自動で作り出せます。
この記事で扱うのは、こうした型を自分で組み立てる側の話です。標準の Partial などを使うだけなら仕組みを知らなくても困りませんが、Mapped Types を理解すると、プロジェクト固有の型変換を自分で定義できるようになります。
基本構文:keyof と in で巡回する
Mapped Types の骨格は { [K in keyof T]: T[K] } です。ここで登場する3つの部品を押さえれば、あとは応用の組み合わせにすぎません。まずはそれぞれの役割を確認しましょう。
| 部品 | 意味 |
|---|---|
keyof T | 型 T のプロパティ名をすべて集めた「キーの一覧(ユニオン型)」を作る |
K in ... | そのキー一覧を1つずつ K に取り出して巡回する(for … in のイメージ) |
T[K] | キー K に対応する値の型を取り出す(インデックスアクセス型) |
実際に、元の型とまったく同じ型を作る「何もしない Mapped Type」を書いてみます。動きを理解するための最小の例です。
interface User {
id: number;
name: string;
}
// User の各プロパティを巡回し、値の型 T[K] をそのまま使う
type Copy<T> = {
[K in keyof T]: T[K];
};
// Copy<User> は { id: number; name: string } と同じ
const user: Copy<User> = { id: 1, name: "太郎" };
keyof User は "id" | "name" というキーの一覧になり、K in keyof User でそれを1つずつ巡回します。各キーについて値の型を T[K] で取り出しているので、結果は元の User と同じ型になります。この「巡回する枠組み」を土台に、値やキーを加工していくのが Mapped Types の使い方です。
値の型を変換する
T[K] の部分を書き換えれば、値の型を一括で変換できます。たとえば「すべてのプロパティを boolean にした型」や「すべてを文字列にした型」を、元の型の形(キー)を保ったまま作れます。フォームの入力チェック結果や、各項目の表示フラグをまとめて持つ型などで役立ちます。
interface Form {
name: string;
age: number;
email: string;
}
// キーはそのまま、値の型をすべて boolean に置き換える
type Flags<T> = {
[K in keyof T]: boolean;
};
// { name: boolean; age: boolean; email: boolean }
const touched: Flags<Form> = {
name: true,
age: false,
email: true,
};
キーの一覧(keyof T)はそのままに、値だけを boolean に差し替えているのがポイントです。元の型に email を追加すれば、Flags 側にも自動で email: boolean が増えます。型の定義を二重管理せずに済むのが、Mapped Types の大きな利点です。
readonly と ? を付ける・外す
Mapped Types では、キーの前に readonly、キーの後ろに ? を書くことで、修飾子(modifier)を一括で付けられます。readonly は読み取り専用、? はオプショナル(省略可能)を意味します。
interface Config {
host: string;
port: number;
}
// すべてのプロパティを readonly かつ省略可能にする
type Loose<T> = {
readonly [K in keyof T]?: T[K];
};
// { readonly host?: string; readonly port?: number }
const c: Loose<Config> = { host: "localhost" }; // port は省略できる
逆に、修飾子を外すこともできます。-readonly と書けば読み取り専用を解除し、-? と書けばオプショナルを解除して「必須」にします。標準の Required 型は、この -? を使って作られています。
interface PartialUser {
readonly id?: number;
name?: string;
}
// readonly と ? をどちらも取り除く
type Strict<T> = {
-readonly [K in keyof T]-?: T[K];
};
// { id: number; name: string } … すべて必須・書き換え可
type Result = Strict<PartialUser>;
-readonly と -? によって、元の型に付いていた readonly と ? が消え、すべてのプロパティが「必須で書き換え可能」になりました。+readonly や +? のように + を明示することもできますが、通常は省略して readonly・? と書くのが一般的です。
as でキー名を付け替える(Key Remapping)
TypeScript 4.1 以降では、[K in keyof T as ...] の形でキー名そのものを変換できます。これを Key Remapping(キーのリマップ)と呼びます。テンプレートリテラル型と組み合わせると、たとえば各プロパティに対応する getter 名を自動生成する、といった型が作れます。
interface Person {
name: string;
age: number;
}
// 各キーを getName / getAge のような名前に付け替える
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
// {
// getName: () => string;
// getAge: () => number;
// }
type PersonGetters = Getters<Person>;
as の後ろに新しいキー名の型を書きます。ここではテンプレートリテラル型 `get${...}` と、先頭を大文字にする Capitalize を使って、name を getName に変換しています。string & K としているのは、キーが文字列であることを保証してテンプレートリテラルに渡すための書き方です。
never を返してキーを除外する
Key Remapping で as never を返すと、そのキーは結果の型から取り除かれます。条件型と組み合わせれば、「特定の型の値を持つプロパティだけ残す/除く」といったフィルタリングができます。
interface Mixed {
id: number;
name: string;
age: number;
}
// 値が string のプロパティだけを残す
type OnlyStrings<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
// { name: string } … number のプロパティは消える
type StringProps = OnlyStrings<Mixed>;
T[K] extends string ? K : never は「値の型が string ならキー K をそのまま、そうでなければ never」という意味です。never になったキーは結果に含まれないため、string 型のプロパティだけが残ります。このように、Mapped Types と条件型を合わせると柔軟な型の絞り込みが可能になります。
標準の型も Mapped Types でできている
ここまで見てきた構文が分かると、Partial や Readonly といった標準のユーティリティ型の正体も見えてきます。これらは特別な組み込み機能ではなく、Mapped Types で書かれた普通の型定義です。実際の定義は次のようになっています。
// すべてのプロパティを省略可能にする
type Partial<T> = {
[K in keyof T]?: T[K];
};
// すべてのプロパティを必須にする(? を外す)
type Required<T> = {
[K in keyof T]-?: T[K];
};
// すべてのプロパティを読み取り専用にする
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
それぞれの違いを並べると、Mapped Types のどの部品を使っているかがよく分かります。修飾子を「付ける・外す」だけで、これだけの型が作られているのです。
| 型 | 使っている仕組み |
|---|---|
Partial<T> | 各プロパティに ? を付けて省略可能にする |
Required<T> | -? で ? を外して必須にする |
Readonly<T> | 各プロパティに readonly を付ける |
標準の型で足りるならそれを使えばよいですが、「値をすべて別の型にしたい」「特定のキーだけ残したい」といった独自の要件が出てきたときは、同じ仕組みで自分の型を書けます。標準の型を暗記するのではなく、作り方を知っておくと応用が利きます。
まとめ
Mapped Types は、{ [K in keyof T]: ... } という構文で既存の型のプロパティを巡回し、新しい型を自動生成する仕組みです。値の部分を書き換えれば型を一括変換でき、キーの前後に readonly や ? を付ける・-readonly や -? で外すことで修飾子を制御できます。さらに as によるキーのリマップを使えば、キー名の変更や never によるプロパティの除外も可能です。Partial や Readonly といった標準の型もこの仕組みで作られており、構文を理解すればプロジェクト固有の型変換を自分で定義できるようになります。型の二重管理を減らし、変更に強いコードを書くための土台として、ぜひ活用してみてください。