型の拡大(Widening)問題
TypeScript が複数の呼び出しサイト引数から型を推論するとき、すべての候補を包含するように型を拡大(widen) することがあります。通常は便利ですが、特定のジェネリクスシナリオでは緩すぎる型が生成され、不正な状態がすり抜けてしまいます。
キーとデフォルト値を受け取る関数を考えます:
function createState<T extends string>(key: T, defaultValue: T) {
return { key, defaultValue };
}
const state = createState("color", "blue");
// T は "color" | "blue" と推論される — 望ましくない
TypeScript は T を "color" | "blue" と推論します。本来は defaultValue が key に一致することを制約したいはずです。
NoInfer の登場
TypeScript 5.4 で導入された NoInfer<T> は、コンパイラに対して この位置を型推論に使わないでください と指示します。型自体は T に対してチェックされますが、T の解決に候補を提供しません。
function createState<T extends string>(
key: T,
defaultValue: NoInfer<T>
) {
return { key, defaultValue };
}
const state = createState("color", "blue");
// T は "color" と推論される
// "blue" は "color" に対してチェックされる — OK
不一致の値を渡すと明確なエラーになります:
createState("color", 42);
// Error: Type 'number' is not assignable to type '"color"'
NoInfer の仕組み
NoInfer<T> は lib.es5.d.ts で次のように定義されています:
type NoInfer<T> = [T][T extends any ? 0 : never];
条件型を使って推論をブロックしています。コンパイラはこの位置を非推論可能と見なします。実際の型はチェック用に T に解決されます。
ユースケース1 — 関数パラメータ
型付きペイロードを持つイベントエミッタ:
function onEvent<E extends string, P>(
event: E,
handler: (payload: NoInfer<P>) => void
): void;
onEvent("click", (p: MouseEvent) => {}); // P = MouseEvent
onEvent("click", (p: number) => {});
// Error: 'number' is not assignable to 'MouseEvent'
ユースケース2 — タプル推論
function pair<T extends readonly any[]>(
first: [...T],
second: NoInfer<[...T]>
): T;
const p = pair([1, "a"], [2, "b"]);
// T は [number, string] と推論 — 正しい
ユースケース3 — ジェネリクス制約
制約型からの推論を防止:
function lookup<T, K extends keyof T>(
obj: T,
key: K,
fallback: NoInfer<T[K]>
): T[K];
const val = lookup({ a: 1, b: "hello" }, "a", 0);
// val: number — 正しい
lookup({ a: 1 }, "a", "wrong");
// Error: 'string' is not assignable to 'number'
代替手段との比較
| アプローチ | 動作 |
|---|---|
| NoInfer なし | すべての候補のユニオンに拡大 |
| 明示的な型パラメータ | 呼び出し元が手動で型を指定 |
| NoInfer | 選択した位置からの推論を防止 |
| 別の型パラメータ | 不必要な複雑さを追加 |
NoInfer を使うべき場面
- 2つ以上のパラメータが型パラメータを共有 し、一方が推論元、もう一方がチェック対象である場合
- 戻り値型の推論を保護 し、呼び出し元引数による拡大を防ぐ場合
- ビルダーパターン — チェーンメソッド間での型汚染を防ぐ場合
class Builder<T extends Record<string, unknown>> {
set<K extends string, V>(
key: K,
value: NoInfer<V>
): Builder<T & Record<K, V>> {
return this;
}
}
まとめ
NoInfer<T> はジェネリクスの推論を厳格化するための焦点特化型ツールです。特定のパラメータ位置を推論対象外とマークすることで不要な拡大を防ぎ、型互換性は維持します。ひとつの引数が型のアンカーとなり、他の引数がそれに従うべき場面で活用してください。結果として、より正確な型、優れた IDE 補完、そして明確なコンパイルエラーが得られます。
