inferとはなにものか?Zodを添えて
infer
とは型推論によって決まる、一時的な型変数の宣言
ユーティリティ型の実装やZodでよくみかけるinfer
とは一体何者なのかをみていきます。
// ユーティリティ型のひとつ、ReturnType型の実装
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any;
先に結論から、infer
とはConditional Types でのみ使用される一時的な型変数(の宣言)のことです。
変数ではありますが、実装側で明示的に型を変数に渡すのではなく、コンパイラが変数の型を推論(infer)して型変数の中身を決定します。このことがvariable などの語ではなくinfer が使用されている理由なのでしょう。
infer
を使用することで、より動的な型付けを実現することができます。
Conditional Types
infer
を知るために、Conditional Types についても少しみてみたいと思います。
Conditional Types は型の条件分岐を行える機能で、T extends U ? A : B
という構文で表されます。三項演算子と同様に、T
型がU
型の部分型であればA
型に、そうでなければB
型になります。
type IsString<T> = T extends string ? true : false
//type NumIsString = false
type NumIsString = IsString<number>
//type LiteralIsString = true
type LiteralIsString = IsString<'str'>
このコード例のisString
型では、型引数T
にstring
型の部分型が与えられた場合にはtrue
型が返され、それ以外の場合にはfalse
型が返されています。
infer
はtrue側の分岐内でしか使用できない
infer
はConditional Types T extends U ? A : B
のU
の部分で宣言します。T
として渡した型がT extends U
をみたすとき、T
型の構造の中で、U
内で宣言したinfer
部分に対応する箇所が型変数の中身として推論され、true側の分岐へと渡されます。
type FlattenArray<T> = T extends (infer InferredType)[] ? InferredType : never;
type A = FlattenArray<number[]>
//type A = number
type B = FlattenArray<string>
//type B = never
このコード例ではT
が(infer InferredType)[]
の部分型であること、つまり何らかの配列型であるとき、infer
が働きます。
T
がnumber[]
の場合には、(infer InferredType)[]
の(infer InferredType)
部分に対応する箇所はnumber
型です。よって、InferredType
はnumber
型であると型推論されてtrue側の分岐へ渡されています。
infer
で宣言した型は、false側の分岐で使用することはできません。これはT extends U
を満たさない場合にはT
とU
の構造が異なる可能性があり、infer
で宣言した型の推論がうまくできない場合があるためですね。
//後半のInferredType部分でコンパイルエラーが発生します。
//Cannot find name 'InferredType'.(2304)
type ErrorInfer<T> = T extends (infer InferredType)[] ? never : InferredType;
T extends U
を満たすことは、U
内でinfer
による型推論ができることを保証する条件になっていたわけですね。[1]
infer
複数のinfer
による一時的な型変数の作成は、単一である必要はなく、複数個作成することも可能です。
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
上のコードでは型引数に「引数が1つの関数型」という制約を課しています。
この形の型が型引数として渡された際に、T extends (arg: infer U) => infer V
の部分から、U
が渡された関数型の引数部分の型、V
が返り値部分の型として推論されます。
その後、(arg: V) => U
という元の関数型と比べて引数と返り値の型が逆になった型が返されています。
infer
が使われているユーティリティ型
TypeScriptに標準で組み込まれている型であるユーティリティ型のうち、Parameters
型やReturnType
型、Awaited
型などの実装にはinfer
が使用されています。以下の記事でも触れていますので、興味があれば実装を確認してみてください。
Zodのinferはなにもの?
Next.js のチュートリアルにも登場するバリデーションライブラリ、Zodにも型を推論する機能としてz.infer<T>
というものがあります。
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;
// }
Schema の型をz.infer<T>
に渡すことで、Schema に対応した型を取得しています。このz.infer
はinfer
と関係があるのでしょうか?
z.infer
の部分の型をVSCodeなどで見てみると、以下のようになっています。
(alias) type infer<T extends ZodType<any, any, any>> = T["_output"]
export infer
また、Zodのz.infer
の実装は以下のようになっています。
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 };
export type { TypeOf as infer }
の部分からも分かる通り、z.infer
のinfer
はTypeOf
の別名となっています。
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;
// }
Zodに登場するinfer
も一時的な型変数を宣言するinfer
が関連しているのではないかと思いましたが、Zodの方のinfer
は型推論の意味で名付けられていただけのようですね。
-
TypeScript の型推論は必ずしも正しい型を推論できるわけではないことには依然注意が必要です。 ↩︎
Discussion