🥂

inferとはなにものか?Zodを添えて

2023/12/25に公開

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型では、型引数Tstring型の部分型が与えられた場合にはtrue型が返され、それ以外の場合にはfalse型が返されています。

inferはtrue側の分岐内でしか使用できない

inferはConditional Types T extends U ? A : BUの部分で宣言します。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が働きます。
Tnumber[]の場合には、(infer InferredType)[](infer InferredType)部分に対応する箇所はnumber型です。よって、InferredTypenumber型であると型推論されてtrue側の分岐へ渡されています。

inferで宣言した型は、false側の分岐で使用することはできません。これはT extends Uを満たさない場合にはTUの構造が異なる可能性があり、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が使用されています。以下の記事でも触れていますので、興味があれば実装を確認してみてください。
https://zenn.dev/axoloto210/articles/advent-calender-2023-day24

Zodのinferはなにもの?

Next.js のチュートリアルにも登場するバリデーションライブラリ、Zodにも型を推論する機能としてz.infer<T>というものがあります。
https://zod.dev/

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.inferinferと関係があるのでしょうか?
z.inferの部分の型をVSCodeなどで見てみると、以下のようになっています。

(alias) type infer<T extends ZodType<any, any, any>> = T["_output"]
export infer

また、Zodのz.inferの実装は以下のようになっています。

zod/lib/type.d.ts
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 };

https://github.com/colinhacks/zod/blob/master/src/types.ts#L49-L53

export type { TypeOf as infer }の部分からも分かる通り、z.inferinferTypeOfの別名となっています。
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は型推論の意味で名付けられていただけのようですね。

脚注
  1. TypeScript の型推論は必ずしも正しい型を推論できるわけではないことには依然注意が必要です。 ↩︎

GitHubで編集を提案

Discussion