react-hook-formとzodを併用した際の型の不一致
ここで型をcastした場合、いくつかのAPIの型が正しくなくなる
例
こういうSchemaを用意して
code:ts
const schema = z.object({
price: z.string().transform(Number)
})
type Schema = z.TypeOf<typeof schema>; // { price: number }
useFormに対して使う
code:ts
const methods = useForm<Schema>({
mode: 'onChange',
resolver: zodResolver(schema),
});
const { watch() } = methods;
このuseFormから得られるwatch()等の型はSchema型になる
code:ts
watch(); // { price: number }
getValues(); // { price: number }
しかし、実際は{ price: string }である
resolverを経由するのはhandleSubmitを通した時だけであるため、それ以外の方法で値を取得すると型がズレる
割と色んな方向でおかしくなる
react-hook-formのAPIで、fieldに値を挿入する際の型の不一致
e.g. defaultValue, reset(), setValues()など
適切な型はzod.input<..>であるべき
例えば、<input type="date" />なfieldを使う場合を考える
EntityではDateを使いたいが、form側ではstringにしないといけない
そこで以下のようにschemaを定義する
code:ts
const DateFromString = z
.string()
.refine(d => dayjs(d).isValid())
.transform(d => dayjs(d).toDate());
const schema = z.object({
date: DateFromString
})
こうすると、defaultValue, reset(), setValues()で求められる型はDateになってしまう
しかし、実際ここで指定すべき値の型はstringである
react-hook-formのAPIで、fieldから値を取得する際の型の不一致
e.g. getValues, watch, useWatchなど
今の(上の)実装を真とした場合は、型はzod.input<..>にすべき
だが、利便性を考えれば実装を変えるなりして、zod.output<..>もほしい
今の実装だと、そもそもzodを通過していないので、
値はzod.input<..>になっているが
型はzod.output<..>になっている
handleSubmit
今のままzod.output<..>で正しい
これは、zodの責務が大きすぎることが問題とも言える
zodが、「validation」と「cast」の両方を担ってしまっている
あるいは、react-hook-formがそれらを同一の機能としてresolverとして提供しているのが問題
といってもこれらは密接している概念なので仕方ない気もする
resolverはあくまでもvalidationをする場所であって、値の変換を行う場所ではない
本来はhandleChangeでやるべきなんだろうなmrsekut.icon
便利を追求しすぎて、個々の責務が雑になっている
hooksの提供者ではなく、利用者が指定するのはなんか歪な感じがするがmrsekut.icon
hooksの提供者は、valueAsDate: trueを使うことを強制できない
でもまあこれで一部の問題は解決できそう
例えば、アプリケーション内ではnumberを使用したいものを考える
使用側ではvalueAsNumber: trueを忘れずに指定する
code:tsx
<input
{...register('number', { valueAsNumber: true })
type="number"
/>
schemaではIntFromStringなどを使わずに素直にz.number()と書く
code:ts
const schema = z.object({
price: z.number().positive().int()
})
こうすれば、watch()などでもnumber型になる
問題点を上げるとすれば、空欄や数字以外のものが入った際にNaNになる
これをnullとして扱うなどのロジックは結局別の箇所に書かないといけない
setValueAs: (v) => v === "" ? undefined : parseInt(v, 10),みたいにすればいいのか
しかし、defaultValuesやreset()などのsetするときの方が解決されないmrsekut.icon
↑どうやってもズレるので、react-hook-formを使うのをやめたmrsekut.icon