iTranslated by AI
NoInfer: A Utility Type for Suppressing Type Inference!?
The NoInfer<T> Type
Starting from TypeScript 5.4, a new utility type called NoInfer has been added.
While most traditional utility types, such as Pick and Partial, create new types from an existing type, the NoInfer type—as the name suggests—is a type designed to suppress type inference by the TypeScript compiler.
Type Inference
In TypeScript, you can explicitly specify a type using a type annotation like const num: number = 1. However, even if you don't provide a type annotation, the compiler automatically infers the type for you.
This is known as type inference. However, there are cases where the type is inferred differently from what the developer intended.
Previously, to get the compiler to infer the intended type, you had to devise complex ways of defining the types, which sometimes resulted in long and complicated code.
The NoInfer type is a utility type that helps reduce such redundant boilerplate.
Suppressing Type Inference with NoInfer
To see how type inference works when using the NoInfer type, let's look at an example of a function that takes two strings as arguments.
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)
The two functions in the code example above differ only in whether the second argument is set to NoInfer<T>.
function func<T extends string>(a: T, b: T): void { }
function noInferFunc<T extends string>(a: T, b: NoInfer<T>): void { }
When you pass the same two strings to these functions, they are inferred as the same function type.
func('abc', 'abc') //function func<"abc">(a: "abc", b: "abc"): void
noInferFunc('abc', 'abc') //function noInferFunc<"abc">(a: "abc", b: "abc"): void
However, if you pass different strings, the inference results change.
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)
In the func function, the argument types are inferred as a union of literal types: "abc" | "123".
The result of inferring a type that can accept both 'abc' and '123' strings within the constraint of a subtype of the string type (T extends string) ends up being the union type "abc" | "123".
In the case of noInferFunc, type inference is disabled for the second argument, leading to a different overall inference result for the function.
While the first argument a is inferred to be of type 'abc', type inference is suppressed for the second argument b. As a result, the type parameter T is inferred solely based on the first argument as type 'abc'.
Because of this, a type error occurs if a value other than a subtype of the type inferred from the first argument is passed to the second argument.
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)
As shown, a constraint has been applied where the type of the second argument must be the same as (or a subtype of) the type of the first argument.
When Not Using NoInfer
// Example written without using the NoInfer type
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)
By adding constraints to the types as shown above, you can ensure that the first and second arguments are inferred as the same type even without using NoInfer.
However, this becomes redundant because of the addition of the type parameter U, which is not used elsewhere.
Using the NoInfer type allows you to write such code more concisely.
Incidentally, if you apply NoInfer to both arguments, the type is inferred as string.
This means that the information from the values passed as arguments is no longer being used for type inference.
// If all arguments are NoInfer, type inference is no longer performed
function allNoInferFunc<T extends string>(a: NoInfer<T>, b: NoInfer<T>): void { }
allNoInferFunc('abc', 'abc')// function allNoInferFunc<string>(a: string, b: string): void
Implementation of NoInfer<T>
If you jump to the definition of NoInfer, you will see the following:
/**
* Marker for non-inference type position
*/
type NoInfer<T> = intrinsic;
intrinsic indicates that it refers to an implementation provided by the TypeScript compiler.
The implementation of the NoInfer type in the compiler is described in the following PR:
In the PR, the NoInfer type is referred to as a marker type, functioning as a marker to prevent the compiler from performing inference.
NoInfer<T> and T Can Be Treated as the Same Type
It is mentioned in the PR that the NoInfer<T> type has no effect on T other than suppressing inference.
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.
The TypeScript compiler tests also include cases to check whether NoInfer is removed and the type is displayed when it doesn't affect inference, such as when T is a primitive type.
Use Cases
It is primarily useful in scenarios where you want to use the inference result of another type parameter as is, similar to the function funcWithoutNoInfer<T extends string, U extends T>(a: T, b: U): void { } example.
By using NoInfer<T> in the place where you want to use the inferred result directly (the position of U in this example), you can define the type without affecting the inference process.
It’s great to be able to treat types like variables.
Discussion