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
return (
);
};
// — 使用例 —
// ✅ 正しい:アイコンのみ
// ✅ 正しい:カスタムノードのみ
// ❌ コンパイルエラー:両方指定してはいけない!
onClick={handleError}
/>
// TypeScriptはここで「両方が同時に満たされることは不可能(never)」であると検知し、
// 開発者に即座にエラーを突きつける。
—
パフォーマンスとコンパイラ負荷に関する注意点
チーフアーキテクトとして、パフォーマンスの懸念にも言及しておこう。
高度な条件付き型やユニオン型を多用すると、TypeScriptの型チェッカー(TSServer)のメモリ消費量が増加し、IDEの補完速度(レスポンス)が低下する原因になる。
- 避けるべき悪手: 深すぎるネストの条件付き型や、無限再帰に近いジェネリクス。
- 推奨するアプローチ: 今回紹介したような、プリミティブな型や直感的なユニオン+`never`の組み合わせは、コンパイラの評価コストが極めて低く、ビルドタイムに悪影響を与えない。プロダクションのコードベースでも安心して採用してほしい。
—
チーフアーキテクトからの提言
「動けばいいコード」を書くフェーズはもう終わった。現代のフロントエンド開発において、型定義は単なるドキュメントではなく、「不正な状態の存在を数学的に証明し、排除するためのコードそのもの」である。
引数に `never` を仕込むというテクニックは、一見すると厳格すぎるように思えるかもしれない。だが、この厳格さこそが、大規模開発におけるリファクタリングの恐怖を消し去り、チーム全体の開発生産性を爆発的に底上げする。
次のコードレビューで、もし誰かが `publishedAt?: string` のような安易なオプショナル引数を書いていたら、こう言って差し入れをしてやってほしい。
「ここに `never` を置いて、不正な状態の余地を完全に焼き払ってくれ」 と。