【入門編】 Distributive Conditional Typesの挙動 – TypeScript実践ガイド

こんにちは!フロントエンドの現場を渡り歩いているチーフアーキテクトの私です。

TypeScriptを書き始めの頃って、「型定義」という強そうな壁に何度もぶつかりますよね。`string`や`number`といった基本的な型を覚えたと思ったら、今度は謎の記号や、聞いたこともない難しい名前の機能が出てきて「うっ…」と頭を抱えた経験、ありませんか?

大丈夫です、安心してください。今日お話しする「Distributive Conditional Types(分配条件型)」という名前は、声に出して読むのも嫌になるくらい呪文じみていますが、やっていることの本質はとってもシンプルで、私たちが普段レジでお買い物をしたり、箱の中身を仕分けしたりする作業となんら変わりません。

今日は、この「分配(ぶんぱい)」という魔法のような仕組みの正体を、身近な例えを交えながら、一緒に紐解いていきましょう!

—

そもそも「Conditional Types」ってなに?

まずは基本の復習から。TypeScriptには、三項演算子(`条件 ? 真のとき : 偽のとき`)に似た書き方で、型を条件分岐させる機能があります。これが Conditional Types(条件型) です。

ざっくり言うと、こんな感じです。

// もし T が string なら “文字だよ”、そうじゃなければ “文字じゃないよ” になる型
type IsString = T extends string ? “文字だよ” : “文字じゃないよ”;

type A = IsString; // “文字だよ” になる
type B = IsString; // “文字じゃないよ” になる

「ほうほう、型をif文みたいに分岐できるんだな」と、ここまでなら初心者の方でも「なるほど」と納得できますよね。問題はこの次に起こるのです。

もし、ここに「ユニオン型(`|`で複数の型をまとめたもの)」を放り込んだら、一体どうなると思いますか?

—

お菓子の詰め合わせパックで考える「分配法則」

ここで登場するのが、今回の主役である Distributive Conditional Types(分配条件型) です。

ちょっと想像してみてください。
あなたは今、お菓子の詰め合わせパックを作っています。そのパックには、「クッキー」と「せんべい」が入っています。

ここで、「もし、そのお菓子が『粉っぽいもの』だったら、全部まとめて一個ずつ小袋に分けなさい」というお告げが下りました。

ユニオン型を条件型に渡すとき、TypeScriptはまさにこれと同じことをやります。
ユニオン型(`クッキー | せんべい`)を条件型にドカンと渡すと、TypeScriptは親切心(あるいは余計なお世話?)から、ユニオンの要素を一つずつバラバラにして、それぞれに条件分岐を適用し、最後に再び合体させるのです。

これが「分配法則(Distributive)」の正体です。数学の授業でやった $2 \times (3 + 4) = (2 \times 3) + (2 \times 4)$ を思い出してください。あれと同じことが、型の上で起きています。

実例で見てみよう

コードで確認してみましょう。先ほどの `IsString` に、`string` と `number` を合体させたユニオン型を渡してみます。

type IsString = T extends string ? “文字だよ” : “文字じゃないよ”;

// string または number を渡す!
type CheckResult = IsString;

頭の中で分配のシミュレーションをしてみましょう。
1. まず、`string | number` がバラバラになります(`string` と `number` に分解)。
2. `string` に対して判定:`string extends string ? …` ➔ `”文字だよ”`
3. `number` に対して判定:`number extends string ? …` ➔ `”文字じゃないよ”`
4. 最後に、結果をもう一度合体させる!

結果、`CheckResult` は `”文字だよ” | “文字じゃないよ”` という新しいユニオン型になります。

このように、ユニオン型が条件型に渡されたときに、自動的に要素ごとにバラして処理される挙動のことを「分配条件型」と呼ぶのです。

—

実務でめちゃくちゃお世話になる `Exclude` の正体

「なるほど、バラして合体するのね。でも、それの何が便利なの?」って思いますよね。

実は、TypeScriptの標準ライブラリに最初から入っている便利な型たちの多くは、この「分配法則」を巧みに利用して作られています。その代表格が `Exclude` です。

`Exclude` は、「型 `T` の中から、型 `U` に当てはまるものを除外する」という超実用的な型です。
例えば、「赤、青、黄色のカラーバリエーションから、赤を除外したい!」というときによく使います。

百聞は一見に如かず、その実装を見てみましょう。驚くほどシンプルですよ。

// Exclude の実際の定義
type MyExclude = T extends U ? never : T;

たったこれだけです! `? never` という見慣れないやつがいますが、これは「もし条件に一致したら、型を消し去る(`never`にする)」という意味です。

では、実際に使ってみましょう。

type MyColors = “赤” | “青” | “黄色”;

// “赤” を除外してみる
type NotRedColors = MyExclude;

これがどう動くか、分配法則のステップで追ってみます。

1. `MyColors` は `”赤” | “青” | “黄色”` なので、これが `T` に入ります。
2. 分配が発動し、一つずつチェックされます:

  • `”赤” extends “赤”` ➔ 一致するので `never`(消滅)
  • `”青” extends “赤”` ➔ 不一致なのでそのまま `”青”`
  • `”黄色” extends “赤”` ➔ 不一致なのでそのまま `”黄色”`

3. 残ったものを合体させます:`never | “青” | “黄色”`

TypeScriptの世界では、`never` とユニオン型を合わせると、`never` は空気のように消えるルールがあります。そのため、最終的な結果はこうなります。

// 最終結果: “青” | “黄色”

すごくないですか? ループ処理を一切書かなくても、型の世界ではこの分配法則のおかげで、ユニオン型のフィルタリングが一瞬でできてしまうんです。

—

ちょっと待って!「分配させたくない」ときはどうする?

ここまで読んで、「分配法則ってめちゃくちゃ便利じゃん!」と思ったあなた。実は、この仕様に頭を悩ませる瞬間もやってきます。

「いや、ユニオンをバラバラにしないで、ユニオン型全体を一つの塊として条件判定に使いたいんだよ!」というケースがあるんです。

そんなときはどうすればいいでしょうか? 答えは拍子抜けするほど簡単です。
`T` をそのまま書かずに、ブラケット(大括弧)で囲って `[T]` のようにタプル型(配列っぽく)にしてあげるだけです。

// 分配させない(タプルで包むテクニック)
type NoDistribute = T extends string ? “文字だよ” : “文字じゃないよ”; // 分配する
type ForceNoDistribute = [T] extends [string] ? “文字だよ” : “文字じゃないよ”; // 分配しない!

// テスト
type Test1 = ForceNoDistribute;
// 結果: “文字じゃないよ” (”string | number” 全体をひとまとめにして string かどうか判定するため)

`[T] extends [string]` と書くことで、TypeScriptは「あ、こいつはバラさずに、このままの形(タプル)で比較してほしいんだな」と察知して、分配をストップしてくれます。これは実務の高度な型定義で非常によく使うテクニックなので、心の片隅に置いておいて損はありません!

—

まとめ:怖がらなくて大丈夫です

今日は、Distributive Conditional Types(分配条件型)についてお話ししました。

  • 条件型にユニオン型を渡すと、自動的に要素ごとにバラして計算される(=分配法則)
  • これを利用して、`Exclude` などの便利なフィルター型が作られている
  • バラしたくないときは、`[T] extends [U]` のようにブラケットで包めば回避できる

最初は呪文のように見えた型定義も、ひとつひとつのルールを紐解いていくと、ちゃんと理にかなった仕組みで作られていることが分かりますよね。

TypeScriptの型エラーにぶつかったとき、「あ、今ここで分配が起きているのかも?」と想像できるようになれば、あなたもう立派なTypeScript使いです。
もし分からなくなったら、いつでもこのページに戻ってきて、お菓子の詰め合わせの例えを思い出してくださいね。

それでは、快適なTypeScriptライフを!

コメント

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