【実務・中級編】関数型における「Never」型の活用:引数に渡してはいけない値を型レベルで排除する – TypeScript コア・型システムの基礎解析バイブル

TypeScriptの型システムにおいて、`never`型ほど「誤解され、かつその真価を発揮できていないプリミティブ」はない。多くのエンジニアは、`never`を「例外を投げる関数の戻り値」や「網羅性チェック(Exhaustiveness Checking)の行き止まり」程度にしか捉えていない。

だが、チーフアーキテクトである私から言わせれば、`never`の本質は「型空間におけるブラックホール(消滅演算子)」であり、関数への入力をコンパイル時に完全に遮断するための最強のゲートキーパーである。

今回は、フロントエンドのコンポーネント設計や非同期API連携の現場において、「特定の条件で呼び出しを絶対に許さない(=渡してはいけない値を型レベルで排除する)」ための極限の型制約テクニックを、プロダクションコードベースで伝授しよう。

—

なぜ「オプショナル引数」や「ユニオンの除外」では不十分なのか?

実務でよくある要件を考えてみよう。
「あるアクション関数は、`type === ‘DRAFT’` の時は `publishedAt` を持ってはいけないが、`type === ‘PUBLISHED’` の時は必須になる」というケースだ。

未熟なコードレビューでよく見かけるのが、以下のような甘い設計だ。

// ❌ 悪夢のようなアンチパターン
type Article = {
type: ‘DRAFT’ | ‘PUBLISHED’;
publishedAt?: string; // オプショナルに逃げている
};

この設計では、`type: ‘DRAFT’` なのに `publishedAt: ‘2023-10-01’` が渡ってきても、TypeScriptは文句を言わない。実行時エラーの温床であり、型安全の放棄だ。

これを防ぐために、「渡してはいけないプロパティの型を `never` にする」というアプローチを取る。`never` 型を持つプロパティには、`undefined` すら代入することが許されない(厳密には、何者も代入できない)。

—

実践:APIクライアントとイベントハンドラにおける `never` の強制

フロントエンドとバックエンドを繋ぐレイヤーで、このテクニックを適用したプロダクションコードを見てほしい。

以下のコードは、イベントの種類(`AnalyticsEvent`)に応じて、ペイロード(`payload`)の型を完全に制御し、「存在してはならないパラメータ」が渡された瞬間にコンパイルエラーを発生させる設計だ。

// — 1. ドメインモデルの定義 —
type ClickEvent = {
action: ‘CLICK’;
payload: {
elementId: string;
// クリックイベントには searchQuery は「存在してはならない」
searchQuery?: never;
};
};

type SearchEvent = {
action: ‘SEARCH’;
payload: {
searchQuery: string;
// 検索イベントには elementId は「存在してはならない」
elementId?: never;
};
};

type ViewEvent = {
action: ‘VIEW’;
// VIEW イベント自体に payload は一切不要(プロパティごと排除したい場合)
payload?: never;
};

type AnalyticsEvent = ClickEvent | SearchEvent | ViewEvent;

// — 2. 型安全なトラッキング関数の実装 —
/

  • 開発現場のコードレビューで「なぜこの設計が必要か」を語るべき核心:
  • 引数の型をユニオンにし、それぞれのペイロード内を `never` で封鎖することで、
  • 呼び出し側のタイポやロジックミスをコンパイラが100%検知する。

/
function trackEvent(
action: T[‘action’],
…args: T[‘payload’] extends undefined ? [] : [payload: T[‘payload’]]
): void {
// 実際の送信処理(モック)
console.log(`[Analytics] ${action}`, args[0]);
}

// — 3. 呼び出し側の挙動(IDEとコンパイラの反応)—

// ✅ 正常系:正しい組み合わせ
trackEvent(‘CLICK’, { elementId: ‘submit-btn’ });
trackEvent(‘SEARCH’, { searchQuery: ‘TypeScript’ });
trackEvent(‘VIEW’); // 引数なしでOK

// ❌ コンパイルエラー:存在してはならないプロパティを渡している
trackEvent(‘CLICK’, {
elementId: ‘submit-btn’,
searchQuery: ‘leak’ // Error: Type ‘string’ is not assignable to type ‘undefined’.
});

// ❌ コンパイルエラー:VIEW にペイロードを渡そうとした場合
trackEvent(‘VIEW’, { elementId: ‘should-not-exist’ });
// Error: Expected 0 arguments, but got 1.

このコードの何が優れているのか?

引数の型定義において、`T[‘payload’] extends undefined ? [] : [payload: T[‘payload’]]` という条件付き型(Conditional Types)を用いている点に注目してほしい。
`VIEW` イベントのようにペイロードが不要な場合、関数の引数リスト自体を空(`[]`)に収束させている。そして、オブジェクト内の不要なキーに対して `never` を指定することで、余計なプロパティの混入を物理的にシャットアウトしている。

—

フロントエンドコンポーネント設計への応用:排他的プロパティ(Exclusive OR)

Reactなどのコンポーネント設計でも、`never` による入力制限は強力な武器になる。
例えば、「アイコン名(`icon`)」か「カスタム要素(`customNode`)」のどちらか一方しか受け付けないボタンコンポーネントを考えてみよう。両方指定されたり、どちらも指定されない状態は型レベルでコンパイルエラーにすべきだ。

type ButtonBaseProps = {
label: string;
onClick: () => void;
};

type IconProps = ButtonBaseProps & {
icon: string;
customNode?: never; // iconがある時はcustomNodeは許さない
};

type CustomNodeProps = ButtonBaseProps & {
icon?: never; // customNodeがある時はiconは許さない
customNode: React.ReactNode;
};

type SmartButtonProps = IconProps | CustomNodeProps;

export const SmartButton: React.FC = ({ label, onClick, icon, customNode }) => {
return (

);
};

// — 使用例 —
// ✅ 正しい:アイコンのみ

// ✅ 正しい:カスタムノードのみ
} onClick={handleLoad} />

// ❌ コンパイルエラー:両方指定してはいけない!
}
onClick={handleError}
/>
// TypeScriptはここで「両方が同時に満たされることは不可能(never)」であると検知し、
// 開発者に即座にエラーを突きつける。

—

パフォーマンスとコンパイラ負荷に関する注意点

チーフアーキテクトとして、パフォーマンスの懸念にも言及しておこう。
高度な条件付き型やユニオン型を多用すると、TypeScriptの型チェッカー(TSServer)のメモリ消費量が増加し、IDEの補完速度(レスポンス)が低下する原因になる。

  • 避けるべき悪手: 深すぎるネストの条件付き型や、無限再帰に近いジェネリクス。
  • 推奨するアプローチ: 今回紹介したような、プリミティブな型や直感的なユニオン+`never`の組み合わせは、コンパイラの評価コストが極めて低く、ビルドタイムに悪影響を与えない。プロダクションのコードベースでも安心して採用してほしい。

—

チーフアーキテクトからの提言

「動けばいいコード」を書くフェーズはもう終わった。現代のフロントエンド開発において、型定義は単なるドキュメントではなく、「不正な状態の存在を数学的に証明し、排除するためのコードそのもの」である。

引数に `never` を仕込むというテクニックは、一見すると厳格すぎるように思えるかもしれない。だが、この厳格さこそが、大規模開発におけるリファクタリングの恐怖を消し去り、チーム全体の開発生産性を爆発的に底上げする。

次のコードレビューで、もし誰かが `publishedAt?: string` のような安易なオプショナル引数を書いていたら、こう言って差し入れをしてやってほしい。

「ここに `never` を置いて、不正な状態の余地を完全に焼き払ってくれ」 と。

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