🔖

既存の型に対してzodのスキーマを実装する

2025/01/31に公開

背景

TypeScriptとzodを組み合わせる場合、一般的にzodのスキーマから型を生成する方法が使われます。

const shapeSchema = z.object({
  name: z.string(),
})

type Shape = z.infer<typeof shapeSchema>

このアプローチは型の作成とスキーマの定義を矛盾なく実装できるため便利ですが、次のような場合に課題が生じます。

  • 外部の型定義ファイルを参照する必要がある場合
  • 型定義が既に存在している場合

そこで、本記事では型を先に定義し、それに合わせてスキーマを実装するアプローチを紹介します。

アプローチ

このアプローチは以下の手順で進めます。

  1. TypeScriptでベースとなる型を定義する
  2. 定義した型に基づいてzodスキーマを実装し、satisfiesを使用して型との整合性をチェックする

型の定義

まず、以下のような型を定義します。

interface Shape {
  name: string
}

interface Rect extends Shape {
  name: 'rect'
  width: number
  height: number
}

interface Circle extends Shape {
  name: 'circle'
  radius: number
}

基底型としてShapeを定義し、そこからRect型とCircle型を派生させています。

型に基づくスキーマの実装

次に、定義した型に対応するzodスキーマを実装します。

import { z } from 'zod'

const shapeSchema = z.object({
  name: z.string(),
}) satisfies z.ZodType<Shape>

const rectSchema = z.object({
  name: z.literal('rect'),
  width: z.number(),
  height: z.number(),
}) satisfies z.ZodType<Rect>

const circleSchema = z.object({
  name: z.literal('circle'),
  radius: z.number(),
}) satisfies z.ZodType<Circle>

satisfies演算子を使用してz.ZodType<T>型との互換性を検証している点に注目してください。
スキーマが型定義と一致しない場合、コンパイルエラーとして検知できます。

このように実装することで、既存の型定義や外部の型定義に対してスキーマを定義することが可能になります。

課題

このアプローチではスキーマと型の整合性を完全には担保できない場合があります。

オプショナルなプロパティ

例えば、Shape型にオプショナルなattributesプロパティを追加した場合

interface Shape {
  name: string
  attributes?: Record<string, unknown>
}

スキーマ定義からattributesを省略してもコンパイルエラーは発生しません。
これはattributesプロパティがオプショナル(省略可能)であるためです。

const shapeSchema = z.object({
  name: z.string(),
  // attributesプロパティの定義漏れ
}) satisfies z.ZodType<Shape>

この問題は「型が同一であるか」をチェックする仕組みを用意することで解決できます。
以下のように型の整合性をチェックするコードをテストファイルに記述します。

type Expect<T extends true> = T

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
    ? true
    : false

type _ = Expect<Equal<Shape, z.infer<typeof shapeSchema>>>

やや煩雑ですが、この方法でスキーマと型の整合性をチェックできます。
上記により、全ての型やスキーマをこの方法で実装するのではなく、前述した課題を解決するために部分的に採用するのが望ましいと考えられます。

まとめ

  • アプローチ
    • TypeScriptの型定義に対して、zodのスキーマを後から実装する
    • スキーマにsatisfies z.ZodType<T>を使用することで、型との整合性をチェックできる
  • 有効なケース
    • 外部ライブラリから提供される型定義を利用する場合
    • プロジェクト内に既存の型定義が存在する場合
  • 課題
    • オプショナルなプロパティがある場合、スキーマの定義漏れを検出できない
      • 型の整合性をチェックするテストコードを実装することで解決できる
Aprender Tech Blog

Discussion