フォーム値はロジックに寄せよ
Article
#感想戦
#設計
#React
#TypeScript
みなさんは、React x TypeScript でフォームを実装するとき、どのように実装していますか?
フォームについてよくよく考えてみると、フォームのそれぞれのフィールドとロジックで持つ値はデータ形式 (いわゆる型) が異なります。
flowchart LR
subgraph Form
subgraph input["input"]
string
end
subgraph multiselect["multiselect"]
stringArray["string[]"]
end
end
subgraph Logic
subgraph name["name"]
name_string["string"]
name_undefined["undefined"]
end
subgraph categories["categories"]
categories_stringArray["string[]"]
categories_undefined["undefined"]
end
end
Logic --> Form
Form --> Logic
Logic では各フィールドの値を string や string[] で、値がない場合は undefined として表現します。
それに対して、<input /> や UI ライブラリによくある <MultiSelect /> コンポーネントは string や string[] で、値がない場合は空の "" や [] として表現します。
この表現の違いをどちらに寄せていくかで、実装の自然さが変わってきます。
Not Good: ロジックを Form に寄せる
ロジックの方が自由度があるために、ロジックを Form に寄せることがよくあるかと思います。 しかしそうすると、型の表現に限界が来てしまい、不自然な実装になってしまったり必須と任意の区別がつけられなくなります。
例: 型の表現の限界

const formSchema = z.object({
title: z.string(),
comment: z.string(),
tags: z.array(z.string()),
categories: z.array(z.string()),
});

... string は文字数をチェック、string[] は配列数をチェックすれば良さそうですね
const formSchema = z.object({
title: z.string().min(1), // 必須チェック
comment: z.string(),
tags: z.array(z.string()).min(1), // 必須チェック
categories: z.array(z.string()),
});
// type FormValues = z.infer<typeof formSchema>;
type FormValues = {
title: string;
comment: string;
tags: string[];
categories: string[];
};

Good: Form をロジックに寄せる
Form をロジックに寄せ、Form と Logic 間の変換を挟むことによって、型の表現の限界を解消し、不自然な実装を避けることができます。
つまり、Logic における値なしの表現を Form の空文字列のままにするのではなく、undefined にする処理を挟みます。(Logic の undefined を Form の空文字列に変換する処理も必要)
例: 型の表現の限界を解消

zod の preprocess を使って、フォームの値をロジックに渡す前に変換します
const formSchema = z.object({
title: z.string(),
comment: z.string(),
tags: z.array(z.string()),
categories: z.array(z.string()),
});
const emptyToUndefined = (values) => {
// 各フィールドの空文字や空配列を undefined に変換する
};
const formResolver = z.preprocess(emptyToUndefined, formSchema);

... どちらも z.string() と z.array(z.string())
で必須チェックはできています
comment と categories を undefined を許容するようにします
const formSchema = z.object({
title: z.string(),
comment: z.string().optional(), // 任意チェック
tags: z.array(z.string()),
categories: z.array(z.string()).optional(), // 任意チェック
});
// type FormValues = z.infer<typeof formSchema>;
type FormValues = {
title: string;
comment?: string | undefined;
tags: string[];
categories?: string[] | undefined;
};
まとめ
フォーム値をロジックに寄せることによって、型の表現の限界を解消し、不自然な実装を避けることができます。
...はじめてチャット形式のまとめ方をしてみましたが、思ったより冗長になりますね。使い所が難しいなぁと思いました。
参考
より詳細の説明として以下の記事が参考になりました。