【入門編】Mapped TypesとType Aliasを組み合わせた「型駆動開発」の極意 – TypeScript コア・型システムの基礎解析バイブル

こんにちは!フロントエンドからNode.jsまで、日々TypeScriptの型システムと格闘している先輩エンジニアです。

TypeScriptを使い始めると、必ずと言っていいほどぶつかる壁がありますよね。それが「インターフェース(`interface`)と型エイリアス(`type`)をどう使い分ければいいのか」、そして「オブジェクトの形を動的にゴニョゴニョ変えるにはどうすればいいのか」という疑問です。

今回は、このテーマの最深部にある「Mapped Types(Mapped Types:マップ型)とType Aliasを組み合わせた型駆動開発の極意」を、一緒に紐解いていきましょう。

ここをクリアすれば、あなたも「なんとなく書いている状態」を卒業し、TypeScriptの型システムを意のままに操る真のフルスタックエンジニアに一歩近づけますよ。それでは、さっそく行ってみましょう!

—

1. そもそも「型駆動開発」ってなに?

多くの人は、まずプログラム(処理)を書いて、後から「そこにどんなデータが流れるか」を型にあてがっていきますよね。これを「実装駆動」と呼びます。

それに対して「型駆動開発」は順番が逆です。
「まず、このドメイン(ビジネスロジック)における完璧な型定義を構築し、コンパイラ(TypeScript)にすべてのルールを強制させた上で、実装を型にパズルのように嵌め込んでいく」アプローチです。

この型駆動開発において最強の武器になるのが、「型エイリアス(`type`)」と、既存のオブジェクトのキーを自在に変換・加工する「マップ型(Mapped Types)」のコンビネーションです。

—

2. 基礎の復習:Interface と Type Alias の決定的な違い

まずは土台固めです。「`interface` と `type` って、どっちを使ってもオブジェクトを定義できるから同じじゃない?」と思っていませんか?

実は、コンパイラの裏側での扱いや拡張性が大きく異なります。

  • `interface`(インターフェース):
  • 「名札」や「契約書」のイメージ。後から同じ名前で宣言すると、自動的にプロパティが統合(マージ)されます(宣言的マージ)。
  • オブジェクトの形状定義に特化しています。
  • `type`(型エイリアス):
  • 「既存の型に新しいあだ名(別名)をつける」イメージ。
  • プリミティブ型、ユニオン型(`A | B`)、タプル型、そして今回主役になる高度な型演算(Mapped Typesなど)を記述できます。

つまり、「動的に型を計算・変形させたい」と思った瞬間から、主役は `type` に切り替わるのです。

—

3. 実践!Mapped Types でオブジェクトを自在に料理する

では、具体的なコードを見ていきましょう。
例えば、Webアプリのフォームを管理する場面を想像してください。APIから取得した初期データや、ユーザーが入力中のデータなど、同じ「ユーザー情報」でも状況によって「すべて必須(Required)」にしたり、「すべて読み取り専用(Readonly)」にしたりしたくなりますよね。

TypeScriptには、標準で `Partial` や `Required` といった組み込みのMapped Typesが用意されていますが、今回はその「裏側で何が起きているのか」を理解するために、自分たちで書いてみましょう。

コード例:独自のMapped Typesを作ってみる

// 1. フォームの「元となるデータ構造」を定義する(interfaceを使用)
interface UserForm {
name: string;
email: string;
age: number;
}

// 2. マップ型を使って、すべてのプロパティを「オプショナル(省略可能)」にする型を作る
// 意味:「K は UserForm のキー全部だよ。それぞれの型をそのままにするよ」という意味
type OptionsFlags = {
[K in keyof T]?: T[K]; // ‘?’ をつけることでオプショナルに変換!
};

// 3. 実際に作った型エイリアスを適用してみる
type DraftUserForm = OptionsFlags;

// 使う側のイメージ:
const draft: DraftUserForm = {
name: “Taro”,
// email と age は省略しても、コンパイルエラーにならない!
};

ここがポイント!

`[K in keyof T]` という構文が、マップ型の心臓部です。

  • `keyof T` で、`UserForm` のキー(`”name” | “email” | “age”`)をユニオン型として取り出します。
  • `in` を使って、そのキーを一つずつループ(マッピング)させています。

これこそが、型エイリアスとマップ型が生み出す魔法の第一歩です。

—

4. さらに踏み込む:キーの動的変換と「型駆動」の真骨頂

さて、ここからが本番です。
単にプロパティをオプショナルにするだけでなく、「特定の命名規則に変換する」という高度な操作をしてみましょう。

例えば、「あるオブジェクトのすべてのプロパティの先頭に `on` をつけ、値の型を『そのプロパティが変更されたときに呼ばれるコールバック関数』に変更したい」という要件を考えてみます。Vue.jsのComposition APIやReactのカスタムフックなどの内部設計でよく使われるテクニックです。

// 元となるイベント定義
interface AppEvents {
click: void;
input: string;
submit: { id: number };
}

// テンプレートリテラル型とMapped Typesを組み合わせた高度な型変換
// K を文字列に限定し、先頭に “on” を付与した新しいキーを生成する
type EventHandlers = {
[K in keyof T as K extends string ? `on${Capitalize}` : never]: (payload: T[K]) => void;
};

// コンパイル時に生成される型(型エイリアスによる導出)
type ConvertedHandlers = EventHandlers;
/
生成される型は実質的に以下のようになります:
{
onClick: (payload: void) => void;
onInput: (payload: string) => void;
onSubmit: (payload: { id: number }) => void;
}
/

// 実装例
const handlers: ConvertedHandlers = {
onClick: () => {
console.log(“Clicked!”);
},
onInput: (text) => {
console.log(“Input text:”, text);
},
onSubmit: (data) => {
console.log(“Submitted ID:”, data.id);
}
};

このコード、どうですか?
元となる `AppEvents` さえ変更すれば、それに連動して `onClick` や `onInput` といったイベントハンドラーの型が完全に自動で追従・再計算されます。これぞまさに、型に仕事をさせる「型駆動開発」の醍醐味です。

—

5. 陥りがちな文法エラーとアンチパターン

初学者の頃、あるいは高度な型を書き始めた頃に誰もがハマる罠があります。ここで事前に回避しておきましょう。

罠1:`interface` でマップ型を書こうとして怒られる

// ❌ NGな例:interface ではMapped Typesの構文(in keyofなど)は使えない!
interface BadExample {
[K in keyof T]: T[K]; // コンパイルエラーになります
}

解決策: オブジェクトの動的変形(Mapped Types)を行うときは、必ず `type`(型エイリアス)を使いましょう。

罠2:読み取り専用(Readonly)や修飾子の外し忘れ・付け間違え

マップ型では、修飾子(`readonly` や `?`)を追加するだけでなく、消す(マイナス修飾子)こともできます。

interface ImmutableUser {
readonly id: number;
readonly name: string;
}

// readonly を強制的に剥ぎ取る型(-? と同様に -readonly も可能)
type Mutable {
-readonly [K in keyof T]: T[K];
}

type EditableUser = Mutable;
// プレフィックスに “-” をつけることで、「readonly属性を取り除く」という指示になります。

この「プラスとマイナスの修飾子」の存在を知らないと、既存のライブラリや厳格な型定義をいじる時に手が止まってしまうので、ぜひ覚えておいてくださいね。

—

まとめ:型は「制約」ではなく「設計図」である

今回は、Mapped Types と Type Alias を組み合わせた「型駆動開発」の極意について解説しました。

  • `interface` は、オブジェクトの基本構造を定義し、拡張(マージ)させる「契約書」。
  • `type` と Mapped Types は、既存の型を材料にして、ルールに従い新しい型を自動生成する「加工工場」。

TypeScriptの型システムは、単なるエラーチェックの道具ではありません。「コードの変更が起きたとき、どこに影響が出るかをコンパイラがすべて自動で計算してくれる最強の設計図」です。

ここをマスターすれば、大規模なフロントエンド開発や複雑なNode.jsのバックエンド設計でも、怖気づくことなくしなやかで堅牢なコードが書けるようになりますよ。

ぜひ、あなたのプロジェクトでも、自分だけの便利な Mapped Types を作ってみてくださいね。それでは、また次の記事でお会いしましょう!

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