🍣

型推論を抑えるためのユーティリティ型!? NoInfer型

2024/03/20に公開

NoInfer<T>

TypeScript 5.4からNoInfer型というユーティリティ型が追加されました。
https://devblogs.microsoft.com/typescript/announcing-typescript-5-4/#the-noinfer-utility-type
PickPartialなど従来のユーティリティ型というのは、与えられた型から新しい型を作るものがほとんどでしたが、今回追加されたNoInfer型は名前の通り、TypeScriptのコンパイラによる型推論を抑えるための型となっています。

型推論

TypeScriptでは、const num: number = 1のように型注釈をつけて明示的に型をつけることができますが、型注釈をつけなかった場合にもコンパイラが自動的に型を推測してつけてくれます。
これが型推論と呼ばれるものですが、実装者側が意図している型とは異なる型として推論されてしまうケースもあります。
いままでは意図した型として推論してもらうために型の付け方を工夫する必要があり、そのために記述が長く複雑になってしまう場合もありました。
NoInfer型はこのような冗長な記述の削減に役立つユーティリティ型となっています。

Noinferで型推論を抑えてみる

NoInfer型を使用するとどのように型推論が行われるようになるのかを、文字列2つを引数にとる関数を例に見てみたいと思います。

noInferExample.ts
function func<T extends string>(a: T, b: T): void { }

function noInferFunc<T extends string>(a: T, b: NoInfer<T>): void { }

func('abc', 'abc') //function func<"abc">(a: "abc", b: "abc"): void
noInferFunc('abc', 'abc') //function noInferFunc<"abc">(a: "abc", b: "abc"): void


func('abc', '123')// function func<"abc" | "123">(a: "abc" | "123", b: "abc" | "123"): void

noInferFunc('abc', '123')// function noInferFunc<"abc">(a: "abc", b: "abc"): void
//Argument of type '"123"' is not assignable to parameter of type '"abc"'.(2345)

上のコード例にある2つの関数は第2引数にNoInfer<T>を設定しているかの点のみが異なっています。

function func<T extends string>(a: T, b: T): void { }

function noInferFunc<T extends string>(a: T, b: NoInfer<T>): void { }

この2つの関数に同じ文字列2つを渡した場合には同じ関数型として型推論されます。

func('abc', 'abc') //function func<"abc">(a: "abc", b: "abc"): void

noInferFunc('abc', 'abc') //function noInferFunc<"abc">(a: "abc", b: "abc"): void

しかし、異なる文字列を渡した場合には推論結果が異なってきます。

func('abc', '123')// function func<"abc" | "123">(a: "abc" | "123", b: "abc" | "123"): void

noInferFunc('abc', '123')// function noInferFunc<"abc">(a: "abc", b: "abc"): void
//Argument of type '"123"' is not assignable to parameter of type '"abc"'.(2345)

func関数では引数の型がリテラル型のユニオン"abc" | "123"と推論されています。
関数の引数として'abc''123'という文字列を受け入れられるような型を、string型の部分型という制約(T extends string)の範囲内で推論した結果が、リテラル型のユニオン"abc" | "123"となってるわけです。

noInferFuncの場合には第2引数に関して型推論を行わないようになっているため、関数の型推論の結果が異なっています。
第1引数aに関しては'abc'型であると推論されていますが、第2引数bについては型推論が抑えられた結果、型引数T'abc'型であると推論されています。
このため、第2引数には第1引数で推論された型の部分型以外の値が渡されると型エラーが発生するようになっています。

noInferFunc('abc', '123')// function noInferFunc<"abc">(a: "abc", b: "abc"): void
//Argument of type '"123"' is not assignable to parameter of type '"abc"'.(2345)

Argument of type '"123"' is not assignable to parameter of type '"abc"'.(2345)
第2引数の型は第1引数の型と同じ型(とその部分型)であるという制約が課せていますね。

NoInferを使わない場合

//NoInfer型を使用せずに記述した場合
function funcWithoutNoInfer<T extends string, U extends T>(a: T, b: U): void { }
funcWithoutNoInfer('abc', 'abc')// function funcWithoutNoInfer<"abc", "abc">(a: "abc", b: "abc"): void

funcWithoutNoInfer('abc', '123')// function funcWithoutNoInfer<"abc", "abc">(a: "abc", b: "abc"): void
//Argument of type '"123"' is not assignable to parameter of type '"abc"'.(2345)

上のように型に制約を追加することで、NoInferを使用せずとも第1引数と第2引数が同じ型と推論されるようにもできます。
しかし、他の箇所で使用されない型引数Uが追加されたことで冗長になってしまっています。
NoInfer型を使用することで、このようなコードを簡潔に書くことができるようになります。


ちなみに引数の両方にNoInferをつけた場合にはstringに型推論されます。
引数として渡された値の情報が型推論に使用されなくなっているわけですね。

// 引数すべてをNoInferとすると型推論が行われなくなっている
function allNoInferFunc<T extends string>(a: NoInfer<T>, b: NoInfer<T>): void { }

allNoInferFunc('abc', 'abc')// function allNoInferFunc<string>(a: string, b: string): void

NoInfer<T>の実装

NoInferの定義元に飛んでみると、以下のようになっています。

node_modules/typescript/lib/lib.es5.d.ts
/**
 * Marker for non-inference type position
 */
type NoInfer<T> = intrinsic;

intrinsicというのはTypeScriptのコンパイラが提供する実装を参照することを表しています。

NoInfer型のコンパイラでの実装は以下のPRに記載されています。
https://github.com/microsoft/TypeScript/pull/56794
PRでは、NoInfer型はmarker typeと呼ばれており、コンパイラが推論を行わないようにする印(マーカー)として機能しています。

NoInfer<T>Tは同一の型として扱える

NoInfer<T>型は推論を抑制するという点以外では、Tに一切の影響を与えないとPR内で言及されています。

Other than blocking inference, NoInfer<T> markers have no effect on T. Indeed, T and NoInfer<T> are considered identical types in all other contexts.

TypeScriptコンパイラのテストにも、Tがプリミティブ型の場合など推論に影響を与えない場合にNoInferが削除されて型が表示されるかというケースが含まれています。

https://github.com/microsoft/TypeScript/blob/309fd3db81955ef7a4dd55a80e333b2b767717a7/tests/baselines/reference/noInfer.errors.txt#L1-L125

使いどころ

主にはfunction funcWithoutNoInfer<T extends string, U extends T>(a: T, b: U): void { }での例のように別の型引数の推論結果をそのまま使いたいシーンで活用できると思います。

推論された結果(Tの型推論結果)をそのまま使用したい箇所(今回の例ではUの箇所)でNoInfer<T>とすることで、推論結果に影響を与えずに型を記述していけます。

型を変数のように扱えるようになり嬉しいですね。

GitHubで編集を提案

Discussion