こんにちは。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
—
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の初学者ではありません。中級者への大きな壁を一つ乗り越えたことになります。自信を持って、どんどん型を書いていきましょうね!