1. ホーム
  2. TypeScript

【TypeScript】NoInfer の使い方|余計な型推論を防いで型引数を固定する

Share

ジェネリック関数を書いていると、「複数の引数から同じ型パラメータを推論させたら、意図より広い型になってしまった」という場面に出くわします。デフォルト値や許可リストを別の引数で受け取るときに起こりがちな問題です。TypeScript 5.4 で追加された組み込みユーティリティ型 NoInfer<T> は、指定した位置を型推論の候補から外すことで、この「余計な推論」を止めて型引数を狙いどおりに固定します。この記事では、NoInfer の基本構文、代表的なユースケース、そして使うべき場面・使わなくてよい場面を、動くコードとあわせて解説します。

NoInfer が解決する「余計な推論」とは

TypeScript のジェネリック関数は、渡された引数から型パラメータを自動で推論します。ふつうはこれが便利なのですが、同じ型パラメータを複数の引数から推論できるとき、思わぬ結果になることがあります。次の例を見てください。色のリストと、その中から選んだデフォルト値を受け取る関数です。

street-light-before.ts
function createStreetLight<C extends string>(
  colors: C[],
  defaultColor?: C,
) {
  return { colors, defaultColor };
}

// colors から C は "red" | "yellow" | "green" と推論してほしい
createStreetLight(["red", "yellow", "green"], "red");

// ところが、リストにない "blue" も通ってしまう
createStreetLight(["red", "yellow", "green"], "blue");

2つ目の呼び出しはエラーになってほしいところですが、実際には通ってしまいます。理由は、TypeScript が colorsdefaultColor両方から C を推論しようとするためです。defaultColor"blue" も推論の材料に含まれるので、C"red" | "yellow" | "green" | "blue" という広い型にまとまり、結果としてリストにない色まで受け入れてしまいます。defaultColor はあくまで colors の中から選ばせたいのに、逆に C を広げる原因になっているわけです。

NoInfer の基本構文

NoInfer<T> は、型パラメータ T を「推論の候補から除外する」ことを TypeScript に伝える組み込みユーティリティ型です。特別な import は不要で、PartialReadonly と同じようにそのまま使えます。使い方は、推論源にしたくない位置の型を NoInfer<...> で包むだけです。

street-light-after.ts
function createStreetLight<C extends string>(
  colors: C[],
  // defaultColor は推論に使わない(C は colors だけから決める)
  defaultColor?: NoInfer<C>,
) {
  return { colors, defaultColor };
}

// OK: "red" は colors に含まれる
createStreetLight(["red", "yellow", "green"], "red");

// エラー: 型 '"blue"' の引数を型 '"red" | "yellow" | "green"' の
// パラメーターに割り当てることはできません。
createStreetLight(["red", "yellow", "green"], "blue");

defaultColorNoInfer<C> にしたことで、TypeScript は Ccolors だけから推論するようになりました。C"red" | "yellow" | "green" に固定され、defaultColor はその型に照合されるだけになります。結果、リストにない "blue" はきちんとエラーになります。ポイントは、NoInfer は型そのものを変えるわけではなく、あくまで「この位置は推論に使わない」という推論の向きだけを制御する点です。

NoInfer と似た組み込み型の位置づけ

NoInfer は少し特殊な立ち位置の型なので、よく使う組み込みユーティリティ型と比べて整理しておきます。値の形を変える型とは目的が異なります。

役割
NoInfer<T>その位置を型引数の推論候補から除外する。型の中身は T のまま変えない
Partial<T>すべてのプロパティを省略可能にする(型の形を変える)
Readonly<T>すべてのプロパティを読み取り専用にする(型の形を変える)

PartialReadonly が「型 T を別の形の型に変換する」のに対し、NoInfer は型の中身には手を付けず、コンパイラの推論アルゴリズムに対するヒントとして働きます。この違いを押さえておくと、どんな場面で候補に挙がる型なのかが見えてきます。

配列とその要素を別々の引数で受け取るとき

もう1つの典型例が、配列と「その要素であってほしい値」を別の引数で受け取るケースです。たとえば、配列から特定の要素を除いた新しい配列を返す関数を考えます。除きたい値は、当然その配列に含まれる要素であるべきです。

remove-item.ts
function removeItem<T>(items: T[], target: NoInfer<T>): T[] {
  return items.filter((item) => item !== target);
}

const fruits = ["apple", "banana", "orange"];

// OK: "banana" は fruits の要素
removeItem(fruits, "banana");

// エラー: "grape" は "apple" | "banana" | "orange" に含まれない
removeItem(fruits, "grape");

targetNoInfer<T> にすると、T は第1引数 items からのみ決まります。もし NoInfer を付けないと、target"grape" も推論に混ざって T が広がり、配列に存在しない値の指定を見逃してしまいます。「片方の引数を型の基準(推論源)にし、もう片方はその型に照合するだけ」という関係を作りたいときに、NoInfer がちょうど役立ちます。

デフォルト値を持つ設定関数での活用

許可された値の集合と、その中から選ぶデフォルト値をまとめて受け取る設定関数でも、同じパターンが効きます。オブジェクトのオプションとして両方を受け取るケースで見てみましょう。

create-select.ts
function createSelect<V extends string>(options: {
  values: readonly V[];
  // 初期値は values に含まれるものだけ許可したい
  initial: NoInfer<V>;
}) {
  return options;
}

// OK: initial は values の中の値
createSelect({
  values: ["small", "medium", "large"],
  initial: "medium",
});

// エラー: "huge" は "small" | "medium" | "large" に含まれない
createSelect({
  values: ["small", "medium", "large"],
  initial: "huge",
});

この createSelect では、Vvalues だけから推論させ、initial はその集合に照合されます。NoInfer がなければ initial の値も V を広げてしまい、選択肢にない初期値が通ってしまいます。フォームや UI コンポーネントの設定のように「候補リスト+その中の1つ」を受け取る API では、この書き方で不正な組み合わせをコンパイル時に弾けます。

NoInfer を使うべきとき・使わなくてよいとき

NoInfer は便利ですが、あらゆるジェネリック関数に付けるものではありません。判断の目安を整理します。

使うと効果的な場面

同じ型パラメータを複数の引数から推論できる状況で、そのうち1つだけを推論の基準にしたいときが出番です。「許可リストとその中のデフォルト値」「配列とその要素」のように、片方を型の基準、もう片方をその型への照合対象にしたい関係で威力を発揮します。これまで見てきた createStreetLightremoveItem がまさにこのパターンです。

使わなくてよい場面

型パラメータの推論源がそもそも1か所しかない関数では、NoInfer は不要です。推論の競合が起きないので、付けても意味がありません。また、複数の引数それぞれから素直に推論してほしい(広い型でよい)ケースでも付ける必要はありません。NoInfer は「意図せず型が広がって、本来エラーにしたい値が通ってしまう」という具体的な問題があるときに、ピンポイントで使う道具だと考えるとよいでしょう。むやみに付けると、かえって型がきつくなりすぎて正当な呼び出しまで弾いてしまうことがあります。

バージョンの注意

NoInfer は TypeScript 5.4 で追加された組み込み型です。それより前のバージョンではグローバルに存在しないため、使うにはコンパイラを 5.4 以上に上げる必要があります。古い環境では、条件付き型を使った自作の NoInfer 相当の型で代用されてきましたが、標準搭載された今は組み込みの NoInfer を使うのが確実です。

まとめ

NoInfer<T> は TypeScript 5.4 で追加された組み込みユーティリティ型で、その位置を型引数の推論候補から除外する働きをします。同じ型パラメータを複数の引数から推論できるとき、片方を NoInfer で包むと、もう片方だけを推論の基準にでき、意図せず型が広がってリストにない値が通ってしまう問題を防げます。許可リストとデフォルト値、配列とその要素のように「片方を基準、もう片方をその型に照合」したい関係で特に有効です。一方、推論源が1か所しかない関数や、広い型のままでよい場面では不要です。型の中身は変えず推論の向きだけを制御する型だと理解し、具体的な問題があるときにピンポイントで使いましょう。

参考ページ