Interactive

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()を使います。