型推論を抑えるためのユーティリティ型!? NoInfer型
NoInfer<T>
型
TypeScript 5.4からNoInfer
型というユーティリティ型が追加されました。
Pick
やPartial
など従来のユーティリティ型というのは、与えられた型から新しい型を作るものがほとんどでしたが、今回追加されたNoInfer
型は名前の通り、TypeScriptのコンパイラによる型推論を抑えるための型となっています。
型推論
TypeScriptでは、const num: number = 1
のように型注釈をつけて明示的に型をつけることができますが、型注釈をつけなかった場合にもコンパイラが自動的に型を推測してつけてくれます。
これが型推論と呼ばれるものですが、実装者側が意図している型とは異なる型として推論されてしまうケースもあります。
いままでは意図した型として推論してもらうために型の付け方を工夫する必要があり、そのために記述が長く複雑になってしまう場合もありました。
NoInfer
型はこのような冗長な記述の削減に役立つユーティリティ型となっています。
Noinfer
で型推論を抑えてみる
NoInfer
型を使用するとどのように型推論が行われるようになるのかを、文字列2つを引数にとる関数を例に見てみたいと思います。
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
の定義元に飛んでみると、以下のようになっています。
/**
* Marker for non-inference type position
*/
type NoInfer<T> = intrinsic;
intrinsic
というのはTypeScriptのコンパイラが提供する実装を参照することを表しています。
NoInfer
型のコンパイラでの実装は以下のPRに記載されています。
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
が削除されて型が表示されるかというケースが含まれています。
使いどころ
主にはfunction funcWithoutNoInfer<T extends string, U extends T>(a: T, b: U): void { }
での例のように別の型引数の推論結果をそのまま使いたいシーンで活用できると思います。
推論された結果(T
の型推論結果)をそのまま使用したい箇所(今回の例ではU
の箇所)でNoInfer<T>
とすることで、推論結果に影響を与えずに型を記述していけます。
型を変数のように扱えるようになり嬉しいですね。
Discussion