z.inferでスキーマから型を作る — input と output の違い
この章の目次開く
Zodを使う最大の利点は、スキーマ1つから型も作れることです。この章ではその取り出し方と、つまずきやすい「入力型と出力型のずれ」を扱います。
z.infer — スキーマから型を取り出す
構文: z.infer<typeof スキーマ>
const userSchema = z.object({
id: z.number(),
name: z.string(),
tags: z.array(z.string()),
});
type User = z.infer<typeof userSchema>;
// { id: number; name: string; tags: string[] }typeof が必要な点に注意してください。userSchema は値なので、型の世界に持ち込むために typeof を挟みます。この書き方は TypeScriptのユーティリティ型の章で扱っている ReturnType<typeof fn> と同じ発想です。
入力型と出力型がずれるケース
ここが本題です。Zodのスキーマには2つの型があります。
| 型 | 意味 |
|---|---|
z.input<typeof スキーマ> | parse に渡す値の型 |
z.output<typeof スキーマ> | parse から返ってくる値の型 |
z.infer<typeof スキーマ> | z.output と同じ(=変換後の型) |
ほとんどのスキーマではこの3つは一致します。ずれるのは、値を変える機能を使ったときです。
transform を使った場合
const lenSchema = z.string().transform((v) => v.length);
type In = z.input<typeof lenSchema>; // string
type Out = z.output<typeof lenSchema>; // number
type Inferred = z.infer<typeof lenSchema>; // number(outputと同じ)文字列を渡すと数値が返るスキーマなので、入口と出口で型が違います。
default を使った場合
const schema = z.object({ name: z.string().default('ゲスト') });
type In = z.input<typeof schema>; // { name?: string | undefined }
type Out = z.output<typeof schema>; // { name: string }.default() があると、入力では省略できて、出力では必ず存在するという関係になります。この違いを実際に確かめると、次のコードが両方とも型エラーなく通ります。
const ok: In = {}; // 入力では name を省略できる
const name: string = (parsed as Out).name; // 出力では必ず string
学習者フォームの初期値を z.infer の型で書いたら、name が必須になって「初期値が入れられない」って怒られたんだけど…
先生それがまさにこのズレ。フォームが扱うのは送る前の値だから z.input が正しい。z.infer(= output)は検証を通った後の型なんだ。
迷ったときの判断基準はこれです。
| 何の型が欲しいか | 使うもの |
|---|---|
| 検証を通した後のデータ(APIの戻り値、DBに保存する値) | z.infer / z.output |
| これから検証に渡す値(フォームの入力値、初期値) | z.input |

型を先に書くか、スキーマを先に書くか
既存の型定義がある場合、「型に合ったスキーマを書けているか」を保証したくなります。satisfies を使う方法があります。
type User = { id: number; name: string };
const userSchema = z.object({
id: z.number(),
name: z.string(),
}) satisfies z.ZodType<User>;こうすると、スキーマが User を満たしていない場合にコンパイルエラーになります。ただし実務では、スキーマを唯一の定義元にして型を導出する方が管理する場所が1つで済みます。既存の型定義がどうしても正である場合(他システムが生成した型など)に限って satisfies を使う、という順序で考えるとよいです。
よくあるハマりどころ
typeof を書き忘れる
z.infer<userSchema> は型エラーになります。z.infer<typeof userSchema> が正しい書き方です。
フォームの型に z.infer を使ってしまう
前述のとおり、.default() や transform があると初期値の型が合いません。フォーム側は z.input を使います。
型を手で書いてスキーマと二重管理する
type User を手書きし、それとは別にスキーマを書くと、片方だけ更新される事故が起きます。第1章で見たとおり、二重管理をなくすのがZodを入れる理由の1つです。
ちゃんと使うためのポイント
-
z.infer<typeof スキーマ>でスキーマから型を導出する。型を手書きしない z.inferはz.output(変換後)と同じtransformや.default()があると入力型と出力型がずれる- フォームの値・初期値の型は
z.input、検証後のデータはz.infer
次の章では、検証に失敗したときに返ってくる ZodErrorの扱いとメッセージの日本語化 を扱います。既定のメッセージは英語なので、ユーザーに見せる前にひと手間必要です。
参考リンク
- Inferring types - Zod — 型推論まわりの公式リファレンス(英語)
- typeof 型演算子 - TypeScript公式 — 値から型を取り出す
typeofの解説(英語)