🎠

Zod + 文字列Union型でバリデーションする

2021/10/21に公開

最近、Blitz.jsでアプリケーションを実装したのをきっかけにバリデーションにはZodというライブラリを使っています。

https://github.com/colinhacks/zod

TypeScriptではTree-shakingなどの問題でEnum(列挙型)が弱いということで、代わりに文字列Union型で値の型を定義して使っています。

const fish = {
  Salmon: "サーモン",
  Tuna: "マグロ",
  Trout: "マス"
} as const
type Fish = keyof typeof fish

しかし、Zodのドキュメントを確認するとEnums, Native Enumsなど列挙型については用意されていますし、Union型(ex: string | number )も用意されていますが、文字列Union型を列挙型の代わりに使うパターンでのバリデーションは用意されていないようです。

ZodのEnums

zod.enum

zod.enum は列挙型のバリデーションを行うにあたって用意されたもので、引数に文字列の配列を渡します。

const fishSchema = zod.enum(["Salmon", "Tuna", "Trout"])
type FishSchema = zod.infer<typeof fishSchema>
// 'Salmon' | 'Tuna' | 'Trout'

or 

const fish = ["Salmon", "Tuna", "Trout"] as const
const fishSchema = zod.enum(fish)

つまり、文字列Union型を定義するためのオブジェクトを渡すことはできません。

zod.nativeEnums

こちらは既存の列挙型を扱う際に使用します。

enum Fruits {
  Apple = "apple",
  Banana = "banana",
  Cantaloupe, // you can mix numerical and string enums
}

const FruitEnum = zod.nativeEnum(Fruits)
type FruitEnum = zod.infer<typeof FruitEnum> // Fruits

FruitEnum.parse(Fruits.Apple) // passes
FruitEnum.parse(Fruits.Cantaloupe) // passes
FruitEnum.parse("apple") // passes
FruitEnum.parse("banana") // passes
FruitEnum.parse(0) // passes
FruitEnum.parse("Cantaloupe") // fails

or

const Fruits = {
  Apple: "apple",
  Banana: "banana",
  Cantaloupe: 3,
} as const

const FruitEnum = zod.nativeEnum(Fruits)
type FruitEnum = zod.infer<typeof FruitEnum> // "apple" | "banana" | 3

FruitEnum.parse("apple") // passes
FruitEnum.parse("banana") // passes
FruitEnum.parse(3) // passes
FruitEnum.parse("Cantaloupe") // fails

こちらは既存のEnumsを渡せます。また、オブジェクトを渡すことも可能です。
これで文字列Union型が扱えそうですが、 parse() メソッドはご覧の通り key ではなく value を列挙値として扱っています。

バリデーションをrefineで詳細に検証する

というわけでこのままでは期待する実装ではできませんので、refine() メソッドを使って以下の方法で対応しました。

const fish = {
  Salmon: "サーモン",
  Tuna: "マグロ",
  Trout: "マス"
} as const
type Fish = keyof typeof fish // "Salmon" | "Tuna" | "Trout"

const fishSchema = zod.string().refine((targetType) => {
  const result = Object.keys(fish).find((type) => type === targetType)
  return !!result
})

type ValidationParams = {
  fishType: Fish
}
function validationFish(fishType: ValidationParams) {
  return fishSchema.parse(fishType)
}

文字列Union型を定義し、実際にバリデーションする関数の引数はこちらを型として指定しておきます。
受け取った値を fishSchema で検証します。
この際に文字列型として最初にバリデーションの定義をしておいて、refine でfishオブジェクトから該当するkeyを探します。該当するものがあればバリデーション可ということで処理します。

大人しくEnumsを使えば… と言いたいところですが、Zodのためにそれをやるのは実装の目的に合わないのでややまどろっこしいですがこの様な処理にしました。
今のところZodのドキュメントを睨めっこしてもこれ以外に方法が思いつかなかったのでもしご存知の方いればぜひ教えてください。

Discussion