TypeScriptでの開発に慣れてくると、ジェネリクス(総称型)の便利さに感動する一方で、「お前、なんで勝手にそんな広い型に推論しちまったんだよ…!」と、コンパイラの親切心(おせっかい)に頭を抱える夜がやってきますよね。
特に中級から上級へステップアップする過程で、この「TypeScriptの推論エンジンとの知恵比べ」に直面するエンジニアは後を絶ちません。
そんなフロントエンドの現場の絶望を、スマートに、そして美しく解決してくれる救世主が、TypeScript 5.4で正式に導入された `NoInfer
今回は、この `NoInfer` がなぜ必要なのか、TypeScriptの裏側でコンパイラがどう動いているのか、そして実務でどう使うべきかを、シニアの私からたっぷり解説していきます。心してついてきてください。
—
1. 現場でよくある悲劇:コンパイラのお節介な型推論
まず、私たちが普段直面する「ジェネリクスの暴走」をコードで見てみましょう。
例えば、共通のコンポーネントや、状態管理のヘルパー関数で、次のような「許容されるテーマカラー」を管理する関数を作ったとします。
// アプリケーションで定義された厳格なカラーパレット
type ThemeColor = ‘primary’ | ‘secondary’ | ‘danger’;
// デフォルト値と、ユーザーが渡したカスタム設定をマージする関数を想定
function createConfig
defaultColor: T,
userColor: T
): { default: T; user: T } {
return { default: defaultColor, user: userColor };
}
// — 実際の使用例 —
// ① 普通に使う分には問題ない
const config1 = createConfig(‘primary’, ‘secondary’);
// 戻り値の型: { default: “primary” | “secondary”; user: “primary” | “secondary”; }
ここまでは平和です。しかし、実務で他の開発者がこの関数を以下のように使ったらどうなるでしょうか?
// ② ユーザーが「うっかり」定義外の色を渡し、さらにデフォルト値もそれに引っ張られてほしい場面
const config2 = createConfig(‘primary’, ‘custom-color-free-text’);
ここでTypeScriptのコンパイラは、あなたのためにこう考えます。
> 「おっと、第2引数に `string` の部分集合である `’custom-color-free-text’` が来たぞ。よし、ジェネリクス `T` を `’primary’ | ‘custom-color-free-text’` という広い型に拡充(Widening)して、両方の引数を受け入れられるようにしてやろう!」
その結果どうなるか。`ThemeColor` 型のような「厳格なリテラル型で縛りたいドメイン」において、`T` が勝手に `string` や謎のカスタム文字列型に広がり、型安全性が完全に崩壊します。「いや、第2引数はサードパーティ製の自由入力だから受け入れたいけど、第1引数の `defaultColor` は厳格な `ThemeColor` のままでいてよ!」という私たちの意図が、見事に踏みにじられるわけです。
—
2. ブラウザとTypeScriptの裏側:型推論は「コンパイル時」のパズル
ここでちょっと裏側の話をしましょう。JavaScriptを実行するブラウザは、もちろんTypeScriptの「型」など1バイトも理解していません。ブラウザが実行するのは、型がキレイさっぱり剥ぎ取られた素のJavaScriptです。
つまり、TypeScriptの型推論(Type Inference)とは、私たちがコードを書いている最中に、TypeScriptの言語サーバー(tsserver)が裏側で必死に解いている「パズル」に過ぎません。
TypeScriptの推論アルゴリズムは、基本的に次のように動きます。
1. 関数に渡された実引数の型を見る。
2. その引数を受け入れられるように、ジェネリクス型パラメータ(`T` など)の候補を探す。
3. 複数の引数に関係する型パラメータがある場合、それらの「共通の親(スーパークsetType)」を見つけようと、勝手に型を広げる(あるいは縮める)。
これが「推論の連鎖(Inference from multiple sites)」と呼ばれる挙動です。便利な反面、今回のように「特定の引数からは推論させたくない(=片方の基準はガチガチに固定したい)」という場面では、このアルゴリズムが足かせになってしまうのです。
—
3. TypeScript 5.4の救世主:`NoInfer` の登場
これまでは、この推論の暴走を防ぐために、以下のような苦し紛れの型ハック(過剰なオーバーロードの定義や、ヘルパー関数の分割など)を書く必要がありました。
// 昔よくやった、オーバーロードによる無理やりな型固定
function createConfig
defaultColor: T,
userColor: string // ここをあえて緩くするが、柔軟性が失われる
): { default: T; user: string };
しかし、TypeScript 5.4以降であれば、そんな泥臭いハックはもう必要ありません。標準で用意されたユーティリティ型 `NoInfer
`NoInfer` の正体
言葉の通り、「これ(No)より推論(Infer)するな」という意思表示をコンパイラに伝えるための組み込み型です。TypeScriptの内部実装としては、以下のような条件付き型(Conditional Types)のトリックを利用して、コンパイラの推論アルゴリズムの対象外から除外しています(※実際にはコンパイラ内部の組み込みプリミティブとして最適化されています)。
それでは、先ほどのコードを `NoInfer` を使ってリファクタリングしてみましょう。
type ThemeColor = ‘primary’ | ‘secondary’ | ‘danger’;
// userColor の部分に NoInfer を挟み込む!
function createConfig
defaultColor: T,
userColor: NoInfer
): { default: T; user: string } {
return { default: defaultColor, user: userColor };
}
この変更によって、コンパイラの動きはどう変わるでしょうか?
1. ジェネリクス `T` は、`defaultColor` からのみ型推論されるようになります。
2. 第2引数の `userColor` に渡される値は、`NoInfer` によって推論の対象外(ブラックボックス)になります。
3. その結果、`userColor` にどんな文字列(たとえば `’custom-color’` や、まったく関係ない文字列)を渡そうとも、ジェネリクス `T` がそれに引っ張られて勝手に拡充されることが一切なくなります。
実際に使うとこうなります:
// ① 正しく ThemeColor(‘primary’) として T が推論される
const config1 = createConfig(‘primary’, ‘secondary’);
// ② 2番目の引数に自由な文字列を入れられるが、
// T は string に引っ張られず、ちゃんと ThemeColor の制約を守らせられる
const config2 = createConfig(‘primary’, ‘custom-color-free-text’);
// 戻り値の型: { default: “primary”; user: string; }
// ③ 1番目の引数に間違った値を入れれば、もちろん即座にコンパイルエラー!
const config3 = createConfig(‘invalid-color’, ‘secondary’);
// ❌ Error: Argument of type ‘”invalid-color”‘ is not assignable to parameter of type ‘ThemeColor’.
どうですか? この「意図した側からだけ型を推論させ、もう片方はただのチェック対象にする」というコントロール感。実務で共通UIライブラリやAPIクライアントのラッパーを書くとき、喉から手が出るほど欲しかった挙動ではないでしょうか。
—
4. 現場で役立つ実践パターン:APIパラメータの型制御
もう少し実務に近い例を見てみましょう。
例えば、APIのエンドポイントと、そのペイロード(リクエストボディ)を受け取る汎用的なロガー関数や送信関数を考えてみます。
// サポートされているAPIのパス一覧
type ApiEndpoint = ‘/api/users’ | ‘/api/posts’ | ‘/api/settings’;
// 各エンドポイントに対応するペイロードの型マッピング
interface EndpointPayloadMap {
‘/api/users’: { name: string; age: number };
‘/api/posts’: { title: string; content: string };
‘/api/settings’: { darkMode: boolean };
}
// 従来の危なっかしい関数
// function sendRequest
この設計だと、次のような問題が起きがちです。
「まずペイロードのオブジェクトを先に変数として定義し、それを関数に渡したい」というケースです。
const myPayload = { title: ‘TypeScript 5.4の魅力’, content: ‘NoInferが最高という話’ };
// エンドポイントと一緒に渡す
sendRequest(‘/api/posts’, myPayload);
これは上手くいきます。しかし、もし開発者がうっかりエンドポイントの指定を間違えたり、あるいは汎用的な関数設計の中で、ペイロードの型からエンドポイント側を勝手に逆引きさせようとして推論の順番が狂ったりすると、コンパイラが発狂して長大なエラーメッセージを吐き出します。
ここで `NoInfer` を活用し、「エンドポイント(`endpoint`)の型を主役に据え、ペイロード側は従属物としてチェックするだけ」と明確に主従関係を定義します。
function sendRequest
endpoint: K,
payload: NoInfer
): void {
console.log(`Sending to ${endpoint}:`, payload);
}
// 使用例
sendRequest(‘/api/users’, {
name: ‘Yamada’,
age: 28,
// ❌ ここで余計なプロパティを入れたり、型を間違えると
// EndpointPayloadMap[‘/api/users’] に厳格に照らし合わされて即座にエラーになる
});
このように、「どの引数(あるいはどのジェネリクス)を型推論の主軸にするか」を開発者が意図通りにコントロールできるのが、`NoInfer` を導入する最大のメリットです。
—
5. シニアからのアドバイス:いつ `NoInfer` を使うべきか?
なんでもかんでも `NoInfer` を貼ればいいというわけではありません。過剰な型制約は、コードベースを無駄に複雑にし、後からコードを読むメンバーを混乱させる原因になります。
次のようなシグナルを感じたら、`NoInfer` の導入を検討してください。
1. 「こっちの引数の型を基準にしたいのに、あっちの引数の型に引っ張られてジェネリクスが汚染されている」と感じたとき。
2. デザインシステムや共通コンポーネントのPropsで、厳格なリテラル型(`’sm’ | ‘md’ | ‘lg’` など)を維持したまま、一部の引数にフォールバックや自由入力を許容したいとき。
3. オーバーロード(Function Overloads)でゴリゴリ書き分けていたコードを、スッキリと単一の関数シグネチャにまとめたいとき。
TypeScriptは私たちのプログラミングを助けてくれる強力な相棒ですが、時にはその親切心が足かせになることもあります。そんなとき、この `NoInfer` という強力な手綱を握っていれば、コンパイラを完全に手懐けることができるはずです。
ぜひ、あなたのチームのコードベースでも取り入れてみてください。コードの堅牢性が一段階上がること間違いなしです。

コメント