【入門編】Type Aliasにおける「Intersection Types」の落とし穴:プロパティの衝突とnever型 – TypeScript コア・型システムの基礎解析バイブル

こんにちは。TypeScriptの深淵へようこそ。
日々コードを書いていると、`type A = B & C` のように「型を合成する」場面は頻繁に出会いますよね。これ、一見すると「型を合体させてパワーアップさせる魔法の杖」のように見えますが、実はTypeScriptの型システムが持つ「論理的な矛盾」を突きつけられる、最も危険な罠が潜んでいる場所でもあるんです。

今日は、インターフェースや型エイリアスで `&`(Intersection Types:交差型)を使う際に、なぜ「プロパティの衝突」が起き、なぜ突如として `never` が現れるのか。その深層心理を紐解いていきましょう。

—

1. Intersection Types(交差型)の本質を理解する

まず、`&` の本質をイメージしてください。
数学の集合論でいう「積集合(AND)」です。AとBを `&` で繋ぐということは、「Aの条件も満たし、かつBの条件も満たさなければならない」という強い制約を課すことを意味します。

type User = { id: number; name: string };
type Admin = { role: string };

// 合成された型は、id, name, role すべてを持つ必要がある
type AdminUser = User & Admin;

const person: AdminUser = {
id: 1,
name: “Alice”,
role: “admin”
};

ここまでは平和ですよね。しかし、問題は「型定義が矛盾し始めたとき」に発生します。

—

2. なぜ `never` は現れるのか?:プロパティの衝突

ここからが本題です。もし、同じプロパティ名で「互換性のない型」を定義したらどうなるでしょう?

type A = { count: number };
type B = { count: string };

type Conflict = A & B;

// 実行して確認!
const errorExample: Conflict = {
count: 10 // ここでエラー!
};

なぜエラーになるのか?
TypeScriptのコンパイラはこう考えます。
「`Conflict` は `count` が `number` である必要がある。同時に `count` が `string` である必要もある。`number` であり、かつ `string` である値なんてこの世に存在しないよね?」

その結果、TypeScriptは「そんな型は実現不可能だ」と判断し、`count` を `never` 型(=決して値が存在しない型)に分類します。

「型の衝突」によるデバッグのコツ

もし大規模なプロジェクトで「なぜかプロパティが `never` になる」という現象に出会ったら、以下の手順で原因を突き止めましょう。

1. ホバーして確認: VSCodeでその型の上にカーソルを乗せます。`count: never` となっていたら、それが「衝突のサイン」です。
2. 分解して考える: `type Result = T1 & T2` なら、`T1` と `T2` の定義元を別々に見て、同じキーを探します。
3. `Pick` で切り出す:

// どの型が原因か特定するデバッグ用コード
type CheckA = Pick; // -> { count: never }

—

3. なぜ `interface` と `type` で挙動が違うのか?

ここがTypeScriptの面白いところです。実は、`interface` 同士の継承(`extends`)と、`type` の交差(`&`)では、衝突時の「優しさ」が違います。

interface I1 { count: number }
interface I2 extends I1 { count: string } // ここでコンパイルエラー!

`interface` は「継承」という文脈で衝突を検知すると、その場で即座にコンパイルエラーとして教えてくれます。 一方で `type` の `&` は、矛盾した定義であっても「型としては定義できるけど、値は絶対に入れられない `never` 型にするよ」と黙認することがあります。

この「黙認」が、型パズルが複雑化した時のデバッグを難しくする原因です。だからこそ、「型を合成するなら、衝突が起きないように設計する」のがプロの流儀なんですね。

—

4. この罠を回避するための「アーキテクトの視点」

では、衝突を避けるにはどうすればいいでしょうか。3つの極意を伝授します。

1. 「継承」よりも「合成」の意識を強く持つ:
`type` で合成する際、共通のプロパティ名が混入しないよう、ネーミングルール(例:`user_id`, `admin_id`)を厳格にする。
2. `Omit` で衝突を打ち消す:
もし既存の型を再利用するなら、衝突するプロパティを事前に抜き出します。

type Fixed = Omit & B; // Aのcountを消してからBをマージする

3. `never` を味方にする:
あえて `never` を利用して「この組み合わせは許さない」という制約を意図的に作ることもあります。これは高度な型安全性を確保するための強力な武器になります。

—

まとめ:TypeScriptを掌握するということ

`never` という型は、決して「失敗」ではありません。それはTypeScriptが「あなたの書いた型定義には論理的な矛盾があるよ」と親切に教えてくれている警告灯なのです。

「なぜ `never` になるんだろう?」と悩んだとき、それはあなたがTypeScriptの厳格な論理演算の海に一歩足を踏み入れた証拠です。その衝突を紐解いていくことで、あなたのコードはより堅牢に、そして予測可能なものへと進化していきます。

ここをクリアできれば、もうあなたはTypeScriptの初学者ではありません。中級者への大きな壁を一つ乗り越えたことになります。自信を持って、どんどん型を書いていきましょうね!

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