3歩進んで2歩下がる

Software Engineer

Remixの複数mutation実装にzodのdiscriminatedUnionを活用する

Remixでデータの更新を行う際に利用するactionですが、1つのリソースに対する複数の更新操作やユースケースごとの更新操作をまとめたくなる場面があります。例えばTODOアプリケーションであれば、アイテムの追加・更新・削除を行いたいケースが考えられます。このような場面でzodのdiscriminatedUnionを使うと便利です。

サンプル実装

追加・更新・削除の操作時に要求されるデータは異なるため、それぞれのスキーマを用意しておきます。 各操作の種類を区別するためのフィールド(以下の例ではcommand)を設定し、それぞれの操作に要求されるスキーマを定義します。

import { z } from 'zod';

export const CreateTodoSchema = z.object({
  command: z.literal('create'),
  item: z.object({
    title: z.string(),
    description: z.string().optional(),
    status: z.enum(['notStarted', 'inProgress', 'pending', 'done']),
  }),
});

export const EditTodoSchema = z.object({
  command: z.literal('edit'),
  item: z
    .object({
      title: z.string(),
      description: z.string(),
      status: z.enum(['notStarted', 'inProgress', 'pending', 'done']),
    })
    .partial(),
});

export const DeleteTodoSchema = z.object({
  command: z.literal('delete'),
});

上記で定義したスキーマを組み合わせてrequest bodyのパースを行います。ここでzodのdiscriminatedUnionの出番です。discriminatedUnion を使うことで、以下のようにスキーマを構成することができます。

import { z } from 'zod';

export async function action({ request }: ActionFunctionArgs) {
  //  Content-Type が application/json の場合
  const body = await request.json();
  const ret = z
    .discriminatedUnion('command', [
      CreateTodoSchema,
      EditTodoSchema,
      DeleteTodoSchema,
    ])
    .safeParse(body);

  if (!ret.success) {
    throw new Error('Unknown form schema');
  }
  if (ret.data.command === 'create') {
    return createTodo(ret.data);
  }
  if (ret.data.command === 'edit') {
    return updateTodo(ret.data);
  }
  if (ret.data.command === 'delete') {
    return deleteTodo(ret.data);
  }
  throw new Error('Unknown command');
}

jsonではなくformDataの場合はzodに渡す前にデータの変換が必要になりますが、RVFConformなどのフォームバリデーションライブラリを使えばその辺りも簡素に書くことができます。

www.rvf-js.io

conform.guide