Zod + 文字列Union型でバリデーションする
最近、Blitz.jsでアプリケーションを実装したのをきっかけにバリデーションには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