iTranslated by AI
Understanding the infer Keyword in TypeScript with Zod
infer is a Temporary Type Variable Declaration Determined by Type Inference
Let's take a look at what exactly infer is, which is often seen in utility type implementations and Zod.
// Implementation of ReturnType, one of the utility types
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
To jump to the conclusion: infer is (a declaration of) a temporary type variable used only within Conditional Types.
While it is a variable, instead of the implementation side explicitly passing a type to the variable, the compiler infers (infers) the type of the variable and determines the content of the type variable. This is likely why "infer" is used instead of words like "variable".
By using infer, you can achieve more dynamic typing.
Conditional Types
To understand infer, let's also take a look at Conditional Types.
Conditional Types is a feature that allows for conditional branching of types, represented by the syntax T extends U ? A : B. Similar to a ternary operator, if type T is a subtype of type U, it results in type A; otherwise, it results in type B.
type IsString<T> = T extends string ? true : false
//type NumIsString = false
type NumIsString = IsString<number>
//type LiteralIsString = true
type LiteralIsString = IsString<'str'>
In the IsString type example, if the type argument T is given a subtype of string, it returns the true type; otherwise, it returns the false type.
infer Can Only Be Used Within the True Branch
infer is declared in the U part of the Conditional Type T extends U ? A : B. When the type passed as T satisfies T extends U, the part of the structure of type T that corresponds to the infer part declared in U is inferred as the content of the type variable and passed to the true branch.
type FlattenArray<T> = T extends (infer InferredType)[] ? InferredType : never;
type A = FlattenArray<number[]>
//type A = number
type B = FlattenArray<string>
//type B = never
In this code example, infer works when T is a subtype of (infer InferredType)[]—in other words, when it is some kind of array type.
When T is number[], the part corresponding to the (infer InferredType) part of (infer InferredType)[] is the number type. Therefore, InferredType is inferred to be the number type and is passed to the true branch.
Types declared with infer cannot be used in the false branch. This is because if T extends U is not satisfied, the structures of T and U may differ, and the type declared with infer may not be able to be inferred correctly.
// A compilation error occurs in the InferredType part in the latter half.
// Cannot find name 'InferredType'.(2304)
type ErrorInfer<T> = T extends (infer InferredType)[] ? never : InferredType;
Satisfying T extends U serves as the condition that guarantees that type inference via infer can be performed within U.[1]
Multiple infers
The creation of temporary type variables using infer does not have to be limited to just one; it is possible to create multiple.
type ReverseFunction<T extends (arg: any) => any> = T extends (arg: infer U) => infer V
? (arg: V) => U
: never
type Reverse = ReverseFunction<(num: number) => string>
//type Reverse = (arg: string) => number
In the code above, a constraint of "a function type with one argument" is imposed on the type argument.
When a type of this form is passed as a type argument, from the T extends (arg: infer U) => infer V part, U is inferred as the type of the passed function's argument, and V is inferred as the type of the return value.
Subsequently, a type (arg: V) => U is returned, where the argument and return value types are swapped compared to the original function type.
Utility Types Using infer
Among the utility types built into TypeScript, infer is used in the implementation of types such as Parameters, ReturnType, and Awaited. I also touch upon this in the following article, so feel free to check the implementation if you are interested.
What is Zod's infer?
Zod, a validation library that also appears in the Next.js tutorial, has a feature called z.infer<T> for inferring types.
import { z } from "zod";
const testSchema = z.object({
num: z.number()
})
type TestSchemaType = typeof testSchema
// type TestSchemaType = z.ZodObject<{
// num: z.ZodNumber;
// }, "strip", z.ZodTypeAny, {
// num: number;
// }, {
// num: number;
// }>
type TestType = z.infer<TestSchemaType>
// type TestType = {
// num: number;
// }
By passing the Schema's type to z.infer<T>, you can obtain the type corresponding to the Schema. Is this z.infer related to the infer keyword?
Looking at the type of the z.infer part in VSCode or similar editors, it looks like this:
(alias) type infer<T extends ZodType<any, any, any>> = T["_output"]
export infer
Furthermore, the implementation of Zod's z.infer is as follows:
export declare type ZodTypeAny = ZodType<any, any, any>;
export declare type TypeOf<T extends ZodType<any, any, any>> = T["_output"];
export declare type input<T extends ZodType<any, any, any>> = T["_input"];
export declare type output<T extends ZodType<any, any, any>> = T["_output"];
export type { TypeOf as infer };
As you can see from the export type { TypeOf as infer } part, the infer in z.infer is an alias for TypeOf.
It behaves exactly the same when used as z.TypeOf.
import { z } from "zod";
const testSchema = z.object({
num: z.number()
})
type TestSchemaType = typeof testSchema
// type TestSchemaType = z.ZodObject<{
// num: z.ZodNumber;
// }, "strip", z.ZodTypeAny, {
// num: number;
// }, {
// num: number;
// }>
type TestType = z.TypeOf<TestSchemaType>
// type TestType = {
// num: number;
// }
I wondered if the infer appearing in Zod was also related to the infer used for declaring temporary type variables, but it seems that Zod's infer was simply named for the meaning of "type inference."
-
Note that it is still important to be aware that TypeScript's type inference is not always able to infer the correct type. ↩︎
Discussion