【zod】「なくてもいいけど、あるならちゃんと」な、バリデーション
こんばんは!
今回は、最近ドツボにハマってしまって時間をかけてしまった実装についての記事です。
紹介したいのはz.optional()
でもなく、z.nullish()
でもなく、z.nullable()
でもなく、あくまでもタイトルにあるように 「なくてもいいけど、あるならちゃんと」 なバリデーションです...👼
「なくてもいいけど、あるならちゃんと」なバリデーション
さて...。「なくてもいいけど、あるならちゃんと」なバリデーション...何をいうとるんでしょうか?
これ以外の呼び名が思いつかず、こんな表現になってしまいましたが今回はこれで通させていただきます。(もし一般的な呼び名を知っている方がいれば、ぜひコメントに...🙇)
例えば、
一つはPCのアドレスが必須入力なTextInput、もう一つはスマホのアドレスを 任意 で登録できるTextInputがあるフォームを想像してみてみましょう。
この時の、「スマホのアドレスを任意で登録できるTextInput」にかけたくなるバリデーションを、「なくてもいいけど、あるならちゃんと」なバリデーションと呼んでいます。
こんなフォームを実現するためのvalidationをzodを使って実装してみましょう。
.optional
・.nullish
・.nullable
じゃだめなの?
ん?最初にあげたz.optional()
・z.nullish()
・z.nullable()
を使用して、想定しているようなバリデーションを実装してみましょう。
const damenaSchema = z.object({
optional: z.string().email().optional(),
nullish: z.string().email().nullish(),
nullable: z.string().email().nullable(),
})
type DamenaSchema = z.infer<typeof damenaSchema>
// type DamenaSchema = {
// optional?: string | undefined; 「email形式でもいいし、undefinedでもいい」
// nullish: string | null; 「email形式でもいいし、nullでもいい」
// nullable?: string | null | undefined; 「email形式でもいいし、undefinedでもnullでもいい」
// }
一見これらのうち、どれでもOKな気がしますが、実はいくつか問題があります。
問題1: controlled/uncontrolledコンポーネント
上記のいずれかで実装した場合、TextInputに値を入力するまでは大丈夫なのですが、入力した途端こんなWarningが出ます。
Warning: A component is changing an uncontrolled input to be controlled. This is likely caused by the value changing from undefined to a defined value, which should not happen. Decide between using a controlled or uncontrolled input element for the lifetime of the component.
「『uncontrolledな<input/>
』 から『controlledな<input/>
』に変わってしまったよ。やめてね。」とReactに怒られてしまいます。
ざっくりいうと、「<input/>
のvalue
にundefined(or null)
を渡している状態から、string
を渡すように変更するのがダメだよ」ということです。(詳細は以下のReactの公式ドキュメントを参照してみてください🙇)
...これじゃあだめですね。
問題2: 入力 -> 削除でおかしくなる。
問題1のReactからのWarningを思い切って無視してみましょう!!!!
...それでも問題は潜んでいます。実は、そもそもやりたいことが実現できていません。
上記のうちのz.optional()
の実装でvalidationをかけた場合、以下のような流れの挙動になります。
状態 | 例 | valid/invalid | 期待通り? | 補足 | |
---|---|---|---|---|---|
1 | 未入力(undefined ) |
valid | ✅ |
undefined 許容なので期待通り |
|
2 | 入力途中 | hoge_ho | invalid | ✅ | まだemail を満たしていないので期待通り |
3 | 入力完了 | hoge_hoge@gmail.com | valid | ✅ |
email を満たしたので期待通り |
4 | 削除中 | hoge_ho | invalid | ✅ |
email を満たさなくなったので期待通り |
5 | 削除完了("" になる) |
invalid | 🚫 |
undefined もemail も満たしていないのでinvalidになっているが、ユーザー的には未入力なので、期待通りではない |
onChange
によって値を更新しているため、入力していた値を削除すると、undefined
ではなく、""
になってしまいます。
schemaでは空文字は許容していません。email
orundefined
しか許容していないからですね。
...どのように実装すればいいのでしょうか。空文字が入力されたのをみてundefined
に置き換える?schemaの方でtransform
などを使用してコネコネする...?
絶対嫌ですね😇僕は嫌です😇
本題: なくてもいいけど、あるならちゃんと
では、「なくてもいいけど、あるならちゃんと」つまり、
- なくてもいいけど...。
- 入力がなければvalid
- 入力を削除し、inputを空にもどせばもちろんvalid
- あるならちゃんと...。
- 1文字でも入力するなら、指定のvalidationを満たさないとinvalid
なvalidationを実装するにはどうすればいいでしょうか?結論から言うと実装例は以下のコードの通りです。
const niceSchema = z.object({
optionalEmail: z.union([
z.literal(""),
z.string().email()
])
})
「空文字(z.literal("")
)」と「email形式(z.string().email()
)」のどちらか(z.union([])
)」という構造で、「なくてもいいけど、あるならちゃんとemail形式」なvalidationを実装することができました!
もちろんz.string().email()
をお好みのものに置き換えれば、「なくてもいいけど、あるならちゃんと〇〇」なvalidationを実装することができます。
まとめ
というわけで、今回は「なくてもいいけど、あるならちゃんと」というバリデーションの実装を行いました。
さて、実装時は困って色々調べてこの方法にたどりついたのですが、この記事を書いた後に公式ドキュメントを散歩していると、そのまんま書いてありました😇
実装に詰まっている時はドキュメントからその記述を見つけられず、解決した途端ドキュメントがみつかる...。
僕以外にも同じく悩んだ人はいるだろうということで、せっかく書いたので供養として残しておきたいと思います。
ありがとうございました!
Discussion