【入門編】 Conditional Typesの分配法則 – TypeScript実践ガイド

こんにちは!TypeScriptの型定義の世界へようこそ。チーフアーキテクトの私です。

日々のフロントエンド開発、本当にお疲れ様です。型エラーの赤い波線にイライラさせられたり、「なんでこんな挙動するんだよ…!」とディスプレイの前で頭を抱えたりしていませんか? 大丈夫です、その泥臭い試行錯誤の数々こそが、あなたを本物のエンジニアに育て上げる最高のスパイスですからね。安心してください、今日は私が優しく、かつ骨太に指南します。

さて、今回のお題は「Conditional Types(条件型)の分配法則」です。
名前を聞くだけで「うっ、数学アレルギーが…」とブラウザを閉じたくなるかもしれませんが、安心してください。お買い物の日常に置き換えれば、拍子抜けするほどすんなり理解できます。一緒に紐解いていきましょう!

—

制限時間つきの「自動仕分け機」をイメージしよう

まずは、TypeScriptの「条件型(Conditional Types)」の基本のおさらいです。
これは三項演算子(`条件 ? 真の場合 : 偽の場合`)に似ていて、「もし〇〇なら××にする、違ったら▲▲にする」という型レベルのif文を作れる強力な機能です。

ここで、ユニオン型(`A | B` のように「どれか一つ」を許容する型)をこの条件型にポイッと投げ入れたとき、TypeScriptの心優しき(余計なお世話とも言う)コンパイラくんは、「おっ、複数まとめてきたね? じゃあ一つずつバラして処理してあげるね!」というお節介を焼きます。これが「分配法則(Distributive Conditional Types)」と呼ばれる現象です。

これって、身近な例で言うと「自動仕分け機」みたいなものです。

例えば、あなたがダンボールいっぱいに「りんご」と「みかん」を詰めて仕分け機に流し込んだとします。仕分け機は「りんごならカゴAへ、みかんならカゴBへ」というルールを持っています。
分配法則が働くと、仕分け機はダンボールを一度ひっくり返し、一個ずつ「これはりんご、これはみかん」と判定して、最終的にそれぞれのカゴに入った結果をもう一度ごちゃ混ぜ(ユニオン型)にして返してくれるのです。

親切ですよね。でも、時には「ダンボール箱ごと」判定してほしい時もあるわけです。この「一個ずつバラされる挙動」が、初学者の私たちが「あれ? 思った型にならないぞ?」と沼にハマる最大の原因なんですよ。

—

コードで見る「勝手にバラされる」現象

百聞は一見にしかず。実際のコードを見てみましょう。
以下のコードをエディタに貼り付けてみてください。

// 型 T が string なら “文字列だよ”、それ以外なら “その他だよ” を返す条件型
type CheckType = T extends string ? “文字列だよ” : “その他だよ”;

// 【実験1】普通の string型を渡してみる
type Result1 = CheckType;
// 予想通り:”文字列だよ”

// 【実験2】string型 と number型 の「ユニオン型」を渡してみる
type Result2 = CheckType;
// さあ、Result2 はどうなるでしょうか?

さて、`Result2` の型は何になると思いますか?
「`string | number` 全体としては string じゃないから、”その他だよ” になるはず!」……そう思いませんでしたか?

残念! ここで分配法則が爆誕します。
TypeScriptは `string | number` を見て、心の中でこうつぶやきます。
「おっ、`string` と `number` の合わせ技だね! じゃあ個別に判定してユニオンで繋いであげよっと」

1. `string extends string ? “文字列だよ” : “その他だよ”` = “文字列だよ”
2. `number extends string ? “文字列だよ” : “その他だよ”` = “その他だよ”

結果、`Result2` の正体は `”文字列だよ” | “その他だよ”` という、なんともカオスなユニオン型になって返ってきます。これが、意図せずバグを生む「分配の罠」です。

—

救世主!分配をピタッと止める「タプル化」の魔法

「いやいや、私はダンボール箱ごと判定してほしいんだよ! 一個ずつバラすな!」
そんな時、どうすればこのお節介な分配法則をストップできるでしょうか?

答えは拍子抜けするほどシンプルです。型を「大括弧(`[]`)で包んでタプル型にする」だけ。

// T を [], 出力も [] で包んで、タプル(配列)の形で条件判定する
type CheckTypeWithoutDistribute = [T] extends [string] ? “文字列だよ” : “その他だよ”;

// さっきと同じユニオン型を渡してみる
type Result3 = CheckTypeWithoutDistribute;
// 結果はどうなる…?

見事に、`Result3` の型は `”その他だよ”` になります!

なぜこれで分配が止まるのか?

これには明確な理由があります。TypeScriptのルールとして、「条件型の対象(`extends` の左側)が、裸の型パラメーター(何も装飾されていない `T`)である場合のみ」分配法則が発動します。

つまり、`[T]` とタプル(配列)のなかに `T` を閉じ込めてしまうことで、TypeScriptは「おっ、コイツは裸の `T` じゃないな。配列の形をしているから、バラさずにそのまま丸ごと判定しよっと」と認識するわけです。お買い物の例えで言うなら、ダンボール箱にガムテープをしっかり貼って、中身をバラせないようにした状態ですね。

実務の現場でも、ユニオン型全体を一つの塊として扱いたいユーティリティ型(例えば `IsUnion` の判定など)を作る際には、この `[T] extends [U]` というテクニックが100%と言っていいほど頻出します。ぜひお守り代わりに覚えておいてください。

—

まとめ:つまずいた時は「あ、バラされてるな」と疑おう

いかがでしたでしょうか?
Conditional Typesの分配法則は、最初は意図しない挙動に頭を悩ませる魔物のように見えますが、仕組みさえ分かってしまえば最高に頼れる相棒です。

  • ユニオン型を渡すと、勝手に1個ずつバラされて処理される(分配法則)
  • 全体を丸ごと判定したい(バラされたくない)時は、`[T] extends [U]` のようにタプルで囲って封印する

この2つを頭の片隅に置いておくだけで、複雑な型定義を見たときの絶望感がスッと軽くなるはずです。
フロントエンドの旅はまだまだ長いですし、TypeScriptの奥は深いですが、こうやって一歩ずつ理解を積み上げていけば必ず手なづけられます。焦らず、自分のペースでコード書いていきましょう!それではまた次回の現場でお会いしましょう、チーフアーキテクトでした。

コメント

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