インターセクション型(&)は「合成の魔法」か、それとも「設計の落とし穴」か?
現場でTypeScriptを書いていると、最初は `interface` や `type` を素直に並べているだけで幸せになれる。しかし、プロジェクトが巨大化し、APIのレスポンスやコンポーネントのPropsが複雑に絡み合い始めると、途端に型定義は迷宮と化す。
そんな時、多くのエンジニアが手にする武器がインターセクション型(Intersection Types)だ。「`A & B` と書けば、両方のプロパティを併せ持った最強の型ができる」——そう信じているなら、少しだけ立ち止まって耳を貸してほしい。
インターセクション型は強力だが、その本質を理解せずに使うと、将来の自分を苦しめる「負の遺産」になりかねないからだ。
—
インターセクション型の基本と「合成」のリアル
インターセクション型は、一言で言えば「集合論的な積」だ。`TypeA & TypeB` は、TypeAが持つすべてのプロパティと、TypeBが持つすべてのプロパティを「どちらも満たすもの」として定義する。
ここで重要なのは、コンパイラがこれをどう扱っているかだ。TypeScriptは静的解析の段階で、これらの型を一つの大きな「匿名オブジェクト型」としてマージする。
type UserBase = { id: string; name: string; };
type UserProfile = { bio: string; avatarUrl: string; };
// 合成の基本形
type User = UserBase & UserProfile;
const user: User = {
id: “u123”,
name: “Alice”,
bio: “Frontend Specialist”,
avatarUrl: “https://example.com/alice.png”
};
ここまでは教科書通りだ。しかし、実務で本当に恐ろしいのは「プロパティの衝突」である。
「衝突」が起きたとき、TypeScriptはどう裁くのか?
もし `UserBase` と `UserProfile` の両方に `id` があったらどうなるか?
type A = { id: string; };
type B = { id: number; };
type C = A & B;
// Cの id は string & number となり、結果的に “never” になる。
`string` であり、かつ `number` である値などこの世には存在しない。だから `never` になる。ブラウザ(JavaScriptのランタイム)が実行時にどうこうする以前に、TypeScriptのコンパイラが「そんな矛盾したオブジェクトは作らせない」と門前払いを食らわせるわけだ。
現場でよくあるミスは、外部ライブラリの型定義をインターセクションで強引に拡張しようとして、この `never` 地獄にハマるパターンだ。「あ、型が合わない」と思って `any` を逃げ道に使うくらいなら、`Omit` で既存の型を間引く勇気を持つべきだ。
実務で使える「賢い合成」のパターン
ただインターセクションを並べるのではなく、実務では以下のように「既存の型をベースに差分を足す」というアプローチがスマートだ。
type BaseProps = {
id: string;
className?: string;
};
// BasePropsを継承しつつ、特定のプロパティを上書き、あるいは追加する
// PickやOmitと組み合わせるのがシニアの流儀
type ButtonProps = BaseProps & {
onClick: () => void;
variant: ‘primary’ | ‘secondary’;
};
// もしidをnumberに強制変更したい場合は、一度間引いてから合成する
type NumericIdProps = Omit
id: number;
};
なぜ「インターセクション」なのか?
`interface` の `extends` と何が違うのか?とよく聞かれる。
結論から言えば、「名前のない型(匿名型)を即席で作りたいとき」にこそインターセクションが輝く。
- `interface`: 拡張性が高く、エラーメッセージも明快。大規模な設計に適している。
- `type &`: 合成が容易で、柔軟性が高い。関数の中のローカルな型定義や、複雑なユーティリティ型を作る際に真価を発揮する。
特に、Reactのコンポーネントにおける「Propsの合成」では、インターセクション型を多用することになるだろう。
—
シニアからのアドバイス:深入りしすぎないこと
インターセクション型は、複雑にすればするほど、エディタ上での「ホバーした時の型表示」が読めなくなる。`TypeA & TypeB & TypeC & …` と連結しすぎると、TypeScriptの型推論のパフォーマンスも落ちるし、何よりコードを読んでいる人間が「結局、最終的に必要なプロパティは何なのか?」を把握できなくなる。
「合成できるからといって、すべてを合成する必要はない」
これが、現場を生き抜くための教訓だ。型定義が長くなりすぎたら、それは「その型が責務を持ちすぎている」というサインかもしれない。そんな時は、一度立ち止まってコンポーネントやドメインの境界を見直してみてほしい。
TypeScriptは、あなたの書いたコードを安全にするためのツールだ。ツールに振り回されるのではなく、ツールを「可読性の向上」という目的のために使いこなす。それができるようになった時、君はもう一段上のフロントエンド・スペシャリストになれるはずだ。
次は、`Pick` や `Partial` などのユーティリティ型とインターセクションを組み合わせて、さらに洗練された型システムを構築する方法について話そうか。また現場で会おう。

コメント