【実務・中級編】TypeScriptのインターセクション型(&)とプリミティブ型の奇妙な関係 – TypeScript コア・型システムの基礎解析バイブル

TypeScriptの型システムは、一見すると直感的な集合論に基づいているように思える。だが、コンパイラの内部実装や型評価のメカニズムに踏み込むと、いくつかの「美しい矛盾」と「厳密な数学的整合性」に直面する。

コードレビューをしていて、ジュニアやミドルクラスのエンジニアがよくハマる罠の一つが、プリミティブ型をインターセクション型(`&`)で結合した際に発生する `never` 型の謎だ。

「なぜ `string & number` は `never` になるのか?」
「なぜ字義通りのリテラル型を組み合わせるとコンパイルエラーすら起きずに消滅するのか?」

今回は、TypeScriptの型チェッカーが内部でどのように型を評価しているのか、その冷徹なまでの論理を紐解き、実務の現場で堅牢な設計を行うための知見を共有しよう。

—

1. プリミティブのインターセクションが `never` になる理由

まず、TypeScriptの型システムにおける `&`(インターセクション)の定義を思い出してほしい。
集合論において、インターセクションは「積集合(AND)」を意味する。つまり、`A & B` は「AであるかつBである」値の集合を指す。

ここで、TypeScriptのプリミティブ型を考えてみる。

type Impossible = string & number;
// 評価結果: type Impossible = never

なぜ `never`(空集合)になるのか?
JavaScript/TypeScriptのランタイムにおいて、ある一つの値が同時に `string`(文字列)であり、かつ `number`(数値)であることは絶対にあり得ない。メモリ上に存在する単一の値が、文字列の表現とIEEE 754倍精度浮動小数点数の表現を同時に満たすことは物理的に不可能だからだ。

TypeScriptの型チェッカーは、この「同時に満たすことが不可能な交差」を検知した瞬間、その型を `never`(どの値も存在し得ない型) に縮退させる。これが、プリミティブのインターセクションが `never` になる根本的な理由である。

—

2. 実務の罠:リテラル型とブランド型の誤用

この挙動が実務で牙を剥くのは、ジェネリクスやユーティリティ型を駆使したコンポーネント設計、あるいは「branded types(ブランド型)」を実装しようとしたときだ。

例えば、IDの型安全性を担保するために、以下のようなコードを書いたとする。

type UserId = string & { readonly __brand: unique symbol };
type PostId = string & { readonly __brand: unique symbol };

// これはコンパイル通る(ベースがどちらもstringのため)
function processIds(id: UserId | PostId) {
// …
}

これはブランド型として正しく機能する(`string` と、それぞれ異なる一意のシンボルを持つオブジェクト型のインターセクションだからだ)。

しかし、もし誤って以下のようなコードを書いたらどうなるか?

// 異なるプリミティブ型をベースにしてブランドを作ろうとした悲劇
type BadId = string & number; // 間違えてこう定義してしまったとする

型チェッカーは文句も言わずにこれを `never` に変換する。結果として、この `BadId` 型を持つ変数にはいかなる値も代入できなくなり、APIから返ってきたデータを流し込もうものなら、ランタイムエラー以前にTypeScriptのコンパイラが容赦なく型エラーを吐き出すことになる。

—

3. 実用パターン:型安全なAPIパラメータと「存在し得ない状態」の排除

では、このプリミティブとインターセクションの挙動を、どう設計に活かすべきか?
結論から言えば、「ドメインモデルにおいて同時に存在してはならない排他的な条件を、型レベルで強制する」ために積極的に利用すべきだ。

以下に、実務のフロントエンド開発・API連携において即座に応用できる、堅牢なプロダクションコードの設計パターンを示す。

プロダクションコード例:排他的な検索フィルターの型設計

例えば、ユーザー検索APIにおいて、「IDによる検索」と「メールアドレスによる検索」は同時に指定できない、あるいは特定のプリミティブな入力モードが排他的であるべきユースケースを想定する。

/

  • 検索クエリのベース定義

/
type SearchByIdQuery = {
mode: ‘ID’;
target: string;
idOnly: true;
};

type SearchByEmailQuery = {
mode: ‘EMAIL’;
target: string;
emailOnly: true;
};

// これらを誤って同時に満たそうとするインターセクションを定義してみる
type InvalidQuery = SearchByIdQuery & SearchByEmailQuery;
// 評価結果:
// mode: ‘ID’ & ‘EMAIL’ -> never
// idOnly: true & false (true & trueだとしても) -> 型の矛盾により全体が never へ崩壊

TypeScriptの強力なところは、オブジェクトのプロパティの型にプリミティブの矛盾が含まれている場合、オブジェクトの型そのものが自動的に `never` に収束する点だ。

これを実務のAPIクライアント設計に応用し、絶対に起きてはならない不正なパラメータの組み合わせをコンパイル時に検知するコードを書こう。

/

  • [チーフアーキテクトの推奨設計]
  • プリミティブの排他性を利用した厳密なAPIパラメータビルダー

/

// プリミティブな値の種別を絞り込むためのブランド
type JpyCurrency = number & { readonly __currency: ‘JPY’ };
type UsdCurrency = number & { readonly __currency: ‘USD’ };

// 型安全なキャスト関数(ランタイムのバリデーションとセットで使用する)
function asJpy(amount: number): JpyCurrency {
return amount as JpyCurrency;
}

function asUsd(amount: number): UsdCurrency {
return amount as UsdCurrency;
}

// 金額計算関数:異なる通貨の直接的なインターセクションを型レベルで拒絶する
function calculateTotal(a: T, b: T): T {
return (a + b) as T;
}

// — 使用例 —
const priceJpy = asJpy(1000);
const priceUsd = asUsd(10);

// コンパイルエラー:
// Argument of type ‘UsdCurrency’ is not assignable to parameter of type ‘JpyCurrency’.
// 異なる通貨の混同を型レベルで完全に防止している
// const errorCalc = calculateTotal(priceJpy, priceUsd);

const successCalc = calculateTotal(priceJpy, asJpy(500)); // OK

—

4. パフォーマンス上の注意点:過剰なインターセクションが招くコンパイル地獄

最後に、パフォーマンスの観点からも重要な言及をしておく。

TypeScriptのコンパイラ(TSServer)は、インターセクション型(`&`)を評価する際、プロパティごとに型の統合や `never` の検出(Reduction)を行う。
特に、巨大なスキーマやライブラリ(ZodやtRPCなどの複雑な型推論を伴うもの)で、意図せずプリミティブなインターセクションが多発すると、型チェッカーのメモリ消費量が増大し、IDEの補完速度(LSPの応答性)が著しく低下する。

  • アンチパターン: 不要なユーティリティ型を重ね掛けし、内部でプリミティブ同士が衝突して `never` になっていることに気づかないまま、巨大な型ツリーを生成する。
  • 解決策: 複雑な型を組む際は、VSCodeなどのエディタ上でホバーし、意図した型(あるいは無駄な `never`)になっていないかをこまめに確認する。また、不要な `&` を避け、Union(`|`)やMapped Typesを適切に選択する。

まとめ

TypeScriptの `string & number = never` という挙動は、単なるバグや気まぐれではなく、「論理的にあり得ない状態を型として表現しない」という型システムの極めて厳格な整合性の表れである。

この挙動を恐れるのではなく、言語仕様の仕様書を読むように深く理解し、コードレビューの現場で「なぜこの型は `never` になるのか?」をロジカルに説明できるようになれば、あなたも真のTypeScriptマイスターへの階段を確実に登っていると言えるだろう。

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