🍜

Zodで既存の型をプロパティに指定したい

2023/03/30に公開

はじめに

こんにちは、クラウドエースの伊藝です。

変数を any として扱うと、プログラムを自由に書くことができます。

しかし、any を使いすぎると型周りでエラーが発生し、プログラムが意図しない動作をしてしまう可能性があります。

それを防ぐために TypeScript や、型をバリデーションするライブラリが開発されています。

バリデーションライブラリの 1 つに Zod があります。

Zod の基本的な紹介としては以下の記事が分かりやすく参考になるかと思います。

TypeScript のゾッとする話 ~ Zod の紹介 ~

Zod のつらいとこ

Zod は独自のスキーマを定義して、それを TypeScript の型に変換して使います。

const PetSchema = z.object({
  name: z.string(),
  age: z.number(),
});

定義した Zod スキーマは新しく定義するスキーマのプロパティとして使うことができます。

const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
  pet: PetSchema,
});

しかし、Zod で定義せずに、自分で定義した型や外部ライブラリのなどの既存の型を Zod スキーマのプロパティに指定することはできません。

interface Pet {
  name: string;
  age: number;
}

// NG
const UserSchema = z.object({
  name: z.string(),
  age: z.number(),
  pet: Pet,
});

既存の型の場合でも、自分で PetSchema を定義して、プロパティ nameage を指定すると解決します。

しかし、Pet が外部ライブラリなどで宣言されていたと仮定します。

その場合、外部ライブラリの Pet に変更がある度に、それに合わせて PetSchema も変更しなければなりません。

また、既存の型のプロパティ数が多いと、それに対応するコストが高くなってしまいます。

解決策

以下のように書くと既存の型を Zod スキーマのプロパティに指定することができます。

const PetSchema = z.record(z.any()).transform((v) => v as Pet);

.transform() で任意の型に変換することができます。

ただ、これだと型が正しいかどうかのバリデーションは行われません。

そのため、必要に応じて型をチェックする処理を追加します。

const PetNameSchema = z.string().min(1);
const PetAgeSchema = z.number().nonnegative();

export const PetSchema = z
  .record(z.any())
  .refine(
    (v) => {
      // プロパティの存在チェック
      if (!("name" in v)) return false;
      const name = v.name;
      if (typeof name != "string") return false;

      if (!("age" in v)) return false;
      const age = v.age;
      if (typeof age != "number") return false;

      return true;
    },
    { message: "Invalid type" }
  )
  .refine(
    (v) => {
      // プロパティ固有のバリデーション
      const pet = v as Pet;
      const result = PetNameSchema.safeParse(pet.name);
      // PetNameSchema のエラーメッセージを表示
      if (!result.success) console.log(JSON.stringify(result));
      return result.success;
    },
    { path: ["pet"], message: "Invalid type" }
  )
  .refine(
    (v) => {
      // プロパティ固有のバリデーション
      const pet = v as Pet;
      const result = PetAgeSchema.safeParse(pet.age);
      // PetAgeSchema のエラーメッセージを表示
      if (!result.success) console.log(JSON.stringify(result));
      return result.success;
    },
    { path: ["age"], message: "Invalid type" }
  )
  .transform((v) => v as Pet);

おわりに

Zod は便利なライブラリですが、既存の型を含めて全ての型を Zod スキーマ化しようとすると、コストが高くなってしまいます。

そのコストをできるだけ小さくするために、本稿のようにすると少しばかり楽をすることができます。

Discussion