【入門編】関数の引数に「Mapped Types」を適用して設定オブジェクトを動的に必須化する – TypeScript コア・型システムの基礎解析バイブル

こんにちは!TypeScriptの型システムの世界へようこそ。
フロントエンドからバックエンドまで、型安全なコードを書く楽しさに魅せられていく時期ですね。

今回は、関数の引数に「Mapped Types(マップドタイプス)」を応用し、設定オブジェクトを動的に必須化するテクニックについてお話しします。

「部分的な設定(一部だけ指定されたオブジェクト)を受け取るんだけど、特定のフラグが立ったときだけ、残りの設定もすべて必須にしたい……」
そんな現場で本当によくある要件を、TypeScriptの型パズルを使ってエレガントに解決する方法です。

ここをクリアすれば、単なる「型のアノテーション(注釈)」を超えた、「型によるロジックの構築」の面白さがグッと見えてきますよ。一緒にバッチリマスターしていきましょう!

—

1. そもそも「Mapped Types」ってなんだっけ?

基本的なおさらいから入りましょう。
Mapped Typesとは、既存の型をベースにして、新しい型を「マッピング(変換)」して作り出す機能です。

イメージとしては、「オブジェクトのプロパティを一つずつ走査して、型をリフォームするループ処理」のようなものです。

// すべてのプロパティをオプショナル(任意)にする標準の Partial も、内部的には Mapped Types で書かれています
type MyPartial = {
[K in keyof T]?: T[K];
};

`[K in keyof T]` という見慣れない構文がありますが、これは「Tが持つすべてのキー(keyof T)を、変数 K として一つずつ取り出す」という意味ですね。

—

2. 今回やりたいこと:条件に応じた「動的な必須化」

例えば、アプリケーションの初期化関数 `initializeApp` を作るとしましょう。

  • 設定オブジェクト(`Config`)を受け取ります。
  • 基本はすべての設定がオプショナル(自由)です。
  • ただし、`strictMode: true` というフラグが有効になった瞬間だけ、通常は任意である `apiKey` や `databaseUrl` が「絶対に指定しなければならない必須プロパティ」に化けさせたいのです。

これを実現するために、関数を受け取る引数の型を動的に変化させてみましょう。

実装コード

まずは、完成されたコードと、それがどう動くのかを見てみてください。

// 1. すべての設定項目のベースとなる型
interface AppConfig {
apiKey?: string;
databaseUrl?: string;
timeout?: number;
strictMode?: boolean;
}

// 2. 「ある条件(ここでは strictMode: true)」のときに、特定のキーを必須化する Mapped Types
// T: 元の型, K: 必須化したいプロパティのキー
type RequireFieldsWhenStrict =
T extends { strictMode: true }
? T & { [P in K]-?: NonNullable } // strictModeがtrueなら、Kのプロパティから「?(オプショナル)」を剥ぎ取る(-?)
: T; // そうでなければ、そのままの型を返す

// 3. 関数定義
// 引数 config の型を、ジェネリクス C を使って動的に評価させます
function initializeApp(config: RequireFieldsWhenStrict) {
console.log(‘アプリケーションを初期化します…’, config);

if (config.strictMode) {
// 型安全に apiKey が存在することが保証されている!
console.log(`API Key: ${config.apiKey.toUpperCase()}`);
}
}

—

3. コードの内部で何が起きているのか?(型評価の裏側)

このコードの肝は、`RequireFieldsWhenStrict` という型エイリアсにあります。ここを少し解剖してみましょう。

1. `K extends keyof T`

  • 「必須化したいプロパティ名(今回の場合は `’apiKey’ | ‘databaseUrl’`)」が、本当に元の設定オブジェクトのキーに含まれているかをコンパイル時にチェックしています。タイポによるバグをここで完全に封殺します。

2. `T extends { strictMode: true } ? … : …`

  • 条件付き型(Conditional Types)と呼ばれる機能です。「もし渡された設定オブジェクト `T` が `strictMode: true` を持っていたら真のルートへ、そうでなければ偽のルートへ分岐する」という、型のif文です。

3. `[P in K]-?`

  • ここが今回の主役である Mapped Types です。 `-?` という見慣れない記号は、「オプショナル修飾子(`?`)をマイナスする(取り除く)」という意味になります。これで「任意」だったプロパティが強制的に「必須(Required)」に書き換わります。
  • さらに `NonNullable` で、万が一 `undefined` や `null` が入り込む余地を完全にシャットアウトしています。

—

4. 実際の使用例と、コンパイラの振る舞い

では、この関数を実際に呼び出すときの挙動を見てみましょう。

パターンA:通常モード(`strictMode` がない、または `false`)

// OK! apiKey がなくてもコンパイルエラーになりません
initializeApp({
timeout: 1000,
strictMode: false
});

  • 解説: `strictMode` が `true` ではないため、`RequireFieldsWhenStrict` の条件分岐は偽(`: T`)になり、元のオプショナルな型のままパスします。

パターンB:厳格モード(`strictMode: true`)なのに、設定をサボった場合

// ❌ コンパイルエラーが発生します!
initializeApp({
strictMode: true,
timeout: 5000
// Error: プロパティ ‘apiKey’ は型 ‘{ strictMode: true; … }’ にありませんが、必須です。
});

  • 解説: 引数に `strictMode: true` を渡した瞬間、TypeScriptのコンパイラは即座に条件付き型を再評価し、`apiKey` と `databaseUrl` を「必須プロパティ(Required)」へと変異させました。そのため、プロパティが足りないとコンパイルエラーで教えてくれるのです。

パターンC:厳格モードで、正しく必須プロパティを渡した場合

// OK!完璧に型安全です
initializeApp({
strictMode: true,
apiKey: ‘secret_12345’,
databaseUrl: ‘postgres://localhost:5432/db’,
timeout: 3000
});

  • 解説: 必要なピースがすべて揃っているため、無事にコンパイルが通り、実行時にも `config.apiKey` を安全に(非undefinedとして)呼び出すことができます。

—

5. 陥りやすい文法エラーと注意点

この高度な型定義を書くとき、初学者がハマりがちなポイントをいくつかシェアしておきますね。

1. ジェネリクス(``)を忘れる

  • 関数側で `config: RequireFieldsWhenStrict` と書いてしまうと、`AppConfig` 自体は `strictMode` が `boolean`(`true` か `false` か確定していない)であるため、意図通りに条件分岐が評価されません。
  • 必ず関数に渡された具体的な引数の型(型引数 `C`)をベースにして型を計算させるのがコツです。

2. `-?` 演算子の位置

  • Mapped Typesでプロパティの修飾子を操作するとき、`[P in K]-?` のように書きます。コロンの右側に置くのではなく、キー名の直後に `-?` を置く点に注意してください。

—

おわりに

お疲れ様でした!
今回は、Mapped Types と条件付き型を組み合わせることで、「引数の値(`strictMode: true`)に応じて、別の引数の型(必須・任意)を動的に変化させる」という、一歩進んだテクニックを解説しました。

最初は呪文のように見えたコードも、コンパイラがどのような順番で型を評価しているのか(型推論の流れ)をイメージできるようになると、TypeScriptを書く手が止まらなくなるほど楽しくなります。

現場のAPI設計や、複雑な設定ファイルを扱うユーティリティ関数を作る際などに、ぜひこの知見を活かしてみてください。あなたの書くコードが、より堅牢で美しいものになることを応援しています!

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