Prisma から Zod
Prisma はスキーマファイルで定義したモデルから、その型を生成してくれます。
model User {
id Int @id @default(autoincrement())
name String
}
import type { Prisma } from '@prisma/client';
import type { User } from '@prisma/client';
// User モデルの型
const user: User = {
id: 1,
name: 'Bob',
};
// User モデルをつくるのに必要なオブジェクトの型 (id は自動採番されるため取り除かれている)
const userCreateInput: Prisma.UserCreateInput = {
name: 'Alice',
};
Zod はスキーマ宣言とバリデーションのためのライブラリです。
基本的には以下のような使い方をします。
const user = {
id: 1,
name: 'Bob',
};
// スキーマ宣言
const User = z.object({
id: z.number(),
name: z.string(),
});
// スキーマバリデーション
if (User.safeParse(user)) {
// user は User スキーマの条件を満たすことを保証される
console.log(`Hello, ${user.name}!`);
}
// スキーマの型を参照
type UserSchema = z.infer<typeof userSchema>;
Zod ファーストでスキーマ宣言することを前提としているのか、既に定義された型に対して Zod スキーマをつくる機能は提供されていません。Prisma と組み合わせる場合、データモデルを先に考えるため、Prisma によって生成された型を Zod へ繋ぐことになります。調べたところ Zod の製作者がユーティリティ関数を公開していました。
import type { z } from 'zod';
export const schemaForType =
<T>() =>
<S extends z.ZodType<T, any, any>>(arg: S) => {
return arg;
};
これを使うことで Zod スキーマを宣言するときに、既存の型と生合成が取れていることを保証できます。
const userSchema = schemaForType<User>()(
z.object({
id: z.number(),
name: z.string();
}),
);
コールバック関数の引数に渡した Zod スキーマが、Generic Parameter として渡した User 型から逸脱しているとエラーになります。次のエラーは Zod スキーマが name
プロパティを持っていないことを警告しています。
Zod から React Hook Form
React Hook Form はフォームバリデーションのためのライブラリです。Zod のようなバリデーション機能を持つライブラリと組み合わせて使うことができます。方法はとても簡単で、インポートした resolver を useForm の引数であるオプションとして渡すだけです。
ユーザーを登録する簡単なフォームを作成してみましょう。
import { schemaForType } from '@/validation';
// Zod 用の resolver をインポート
import { zodResolver } from '@hookform/resolvers/zod';
import { useForm } from 'react-hook-form';
import { z } from 'zod';
import type { Prisma } from '@prisma/client';
import type { SubmitHandler } from 'react-hook-form';
// Prisma によって生成された型から Zod スキーマを宣言
const userSchema = schemaForType<Prisma.UserCreateInput>()(
z.object({
name: z.string(),
}),
);
// Zod スキーマから型を参照
type UserSchema = z.infer<typeof userSchema>; // { name: string }
const UserForm = () => {
const {
handleSubmit,
register,
formState: { errors },
// useForm にフィールドの型が { name: string } であることを伝える
} = useForm<UserSchema>({
// オプションとして zodResolver を渡す
resolver: zodResolver(userSchema),
});
const onSubmit: SubmitHandler<UserSchema> = (data) => console.log(data);
return (
<form onSubmit={handleSubmit(onSubmit)}>
<label>
Name:
<input type="text" {...register('name')} />
</label>
{errors.name && <div role="alert">{errors.name.message}</div>}
</form>
);
};
export default UserForm;
動作検証
User モデルに変更が入ったときの挙動を検証します。
スキーマファイル内のモデルにフィールドを追加して、変更を Prisma に伝えます。
model User {
id Int @id @default(autoincrement())
name String
+ email String
}
npx prisma db push
フォームコンポーネントを見ると、しっかりエラーになってくれています!
しかし、型レベルではフォームに email
フィールドを追加するところまでを強要できないようです。
おわりに
今回はデータモデルの型をフォームまで電波させる方法を紹介しました。本当は Prisma の型から Zod スキーマを自動生成するようなライブラリを使おうと考えていたのですが、メンテナンスが止まることのリスクを加味してユーティリティ関数の方を採用しました。若干の 2 重管理感は否めませんが、型の整合性をとることが目的なのでこれで満足しています。