Zodの型制約
ランタイムのバリデーションにはZodを使用し、手動の型定義との二重管理を避けます。
スキーマから型を導出する
Zodスキーマからz.inferで型を導出します。スキーマと型定義を別々に管理すると乖離が生じるため、弊社ではスキーマを single source of truth とします。
// スキーマを定義する
const userSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
})
// スキーマから型を導出する
type User = z.infer<typeof userSchema>
このUser型は{ id: string; name: string; email: string }と同等です。スキーマを変更すれば型も自動的に追従します。
バリデーションとparse
parseは不正な値に対して例外を投げます。外部からの入力など失敗が想定される場面ではsafeParseを使い、成功・失敗をオブジェクトで受け取ります。
// 例外を投げる(信頼できるデータ向け)
const user = userSchema.parse(data)
// 成功・失敗をオブジェクトで返す(外部入力向け)
const result = userSchema.safeParse(data)
if (result.success) {
// result.data は User 型に絞り込まれる
console.log(result.data.name)
}
safeParseの戻り値は discriminated union になっており、successで分岐するとdataの型が自動的に絞り込まれます。
スキーマを受け取る関数の型制約
Zodスキーマを引数に取る汎用関数を書く場合、z.ZodTypeで制約をかけます。どんなスキーマでも受け入れる場合と、特定のプロパティを必須にする場合で書き方が異なります。
どんなスキーマでも受け入れる
z.ZodTypeを制約にすると、任意の構造のスキーマを渡せます。汎用的なバリデーションユーティリティに適しています。
function validate<T extends z.ZodType>(schema: T, data: unknown) {
return schema.safeParse(data)
}
validate(z.string(), "hello") // OK
validate(z.object({ id: z.string() }), data) // OK
特定のプロパティを必須にする
ベースとなるスキーマを定義し、typeofで制約をかけることで、特定のプロパティを持つスキーマだけを受け入れます。追加のプロパティは許容されますが、ベースのプロパティが欠けるとコンパイルエラーになります。
const baseSchema = z.object({
id: z.string(),
})
function fn<T extends typeof baseSchema>(schema: T) {
return schema
}
typeof baseSchemaを制約に使うことで、idプロパティを持つスキーマだけが渡せるようになります。
// OK:idを含んでいる
fn(z.object({ id: z.string(), name: z.string() }))
// エラー:idがない
fn(z.object({ name: z.string() }))
optionalとdefaultの使い分け
optional()は値がundefinedでも通しますが、default()はundefinedのときにデフォルト値を補完します。APIのレスポンス型とリクエスト型で使い分けます。
// リクエスト:省略時にデフォルト値を補完する
const requestSchema = z.object({
page: z.number().default(1),
perPage: z.number().default(20),
})
// レスポンス:値が存在しない場合がある
const responseSchema = z.object({
description: z.string().optional(),
})
default()を使うと、parse後の型からundefinedが除外されます。省略可能だが必ず値が欲しい場面ではoptional()ではなくdefault()を使います。