どうも、アドカレの時期だけ記事を投稿するマンです🧘
去年に引き続き今年もこれと言ったネタがありませんが、自分の学んだことをつらつらと書きます。
(ちなみに)
去年のアドカレ記事でご紹介したライブラリ Lenis
は既にメジャーバージョンがリリースされており Weekly Downloads も 10K超え(2023/11/23 - 2023/11/29)と、かなりプロダクション環境にも使えるようになっていそうです👀(詳しくは以下の記事をご覧ください)
はじめに
REST API のような既存の型情報がない場合は、zod のスキーマを正とした実装が可能です。それに対して Prisma は DB ソリューションと型生成が一つになっています。
そのため Prisma のスキーマ を Single Source Of Truth とする設計に対して、Prisma スキーマから生成される型情報に沿っている事を担保する zod の schema を実装します。
(これだけではあまりにも内容が薄すぎたので、Fragment colocation ライクな実装という別トピックも記載しましたw)
結論
TypeScript4.9 から追加された satisfies
演算子を使う事で、型の Widening を抑えつつ簡単に zod のスキーマが既存型情報に沿っていることを担保 & コロケーションが実現できます。
model User {
id String @id @default(uuid())
name String
imageUrl String @db.Text
email String @db.Text
isPremium Boolean @default(false)
servers Server[]
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
上記の prisma のスキーマがある場合における User
に対して新規レコードを作成する場合のzodスキーマは、以下のように実装できます。
import { z } from 'zod';
import { Prisma } from '@prisma/client';
// ✅
const UserCreateSchema = z.object({
name: z.string(),
imageUrl: z.string(),
email: z.string(),
isPremium: z.boolean().optional(),
}) satisfies z.ZodType<Prisma.UserCreateInput>;
// ❌
const UserCreateSchema = z.object({
name: z.string(),
imageUrl: z.string(),
email: z.number(), // error : 型 'number' を型 'string' に割り当てることはできません。
isPremium: z.boolean().optional(),
}) satisfies z.ZodType<Prisma.UserCreateInput>;
ご存知の方は多いかと思いますが、ここで出てきているUserCreateInput
という型は、Prisma Client のヘルパー型を使用して動的に生成されている型情報になります。
Prisma Client のヘルパー型 とは?
npx prisma generate
を実行した際に生成されるデータモデルの型は、Prisma Client のヘルパー型を用いて動的に生成されています。そして型変数名は MODAL_NAME
にはスキーマに定義したデータモデル名が割り当てられるようになっています。
- Prisma.<MODAL_NAME>DefaultArgs
- Prisma.<MODAL_NAME>GetPayload
- Prisma.<MODAL_NAME>Select
- Prisma.<MODAL_NAME>CountAggregateInputType
// データモデル名が User の場合
- Prisma.UserDefaultArgs
- Prisma.UserGetPayload
- Prisma.UserSelect
- Prisma.UserCountAggregateInputType
Fragment colocationとは
コロケーション(colocate) は 文字通り、 co(一緒に) + locate(配置する) という意味であり、コンポーネント内で必要なデータをコンポーネントと同じ場所で宣言する手法です。
GraphQL の文脈では、コンポーネントが必要なデータを GraphQL の Fragment
で記述し、コンポーネント内で宣言する事を指します。
Fragment colocation を使うことで、親コンポーネントは、子コンポーネントそれぞれで宣言された Fragment
を取りまとめて、単一クエリを投げるという形を取ります。
そうすることで、親コンポーネントは子コンポーネントが子コンポーネントで宣言されたフラグメントだけを知っていれば良いため、どのようなフィールドが必要かを知る必要がなくなります。
要するに、 コンポーネントが必要とするデータ(クエリ)はそのコンポーネント内で管理しようぜという事です。
Fragment colocation ライクな使い方をしてみる
Prisma Client のヘルパー型 と satisfies
演算子を用いて、Next.js(App Router) で以下の3つのコンポーネントを Fragment colocation ライクに実装してみます。
- ログインユーザーの詳細一覧を表示するページ (
UserDetail.tsx
) ← 親コンポーネント - ログインユーザーが参加しているサーバー情報一覧を表示するコンポーネント (
ServerDetail.tsx
) - ログインユーザーがフォローしているメンバーを表示するコンポーネント (
MemberDetail.tsx
)
なお、今回使用したライブラリのバージョンは以下の通りです。
"next": "13.5.6",
"typescript": "5.3.2",
"prisma": "^5.6.0",
"@prisma/client": "^5.6.0",
// ログインユーザーがフォローしているメンバーを表示するコンポーネント
import { Prisma } from "@prisma/client";
export const MemberDetailFragment = {
members: {
select: {
id: true,
role: true,
createdAt: true,
updatedAt: true,
profile: true,
},
},
} satisfies Prisma.UserSelect;
type MemberDetailFragment = Prisma.UserGetPayload<{
select: typeof MemberDetailFragment;
}>;
const MemberDetail = ({ members }: { members: MemberDetailFragment }) => {
return (
<div>
<h2>Member Detail</h2>
<ul>
{members.members.map((member) => (
<li key={member.id}>{member.profile.name}</li>
))}
</ul>
</div>
);
};
// ログインユーザーが参加しているサーバーを表示するコンポーネント
import { Prisma } from "@prisma/client";
export const ServerDetailFragment = {
servers: true,
} satisfies Prisma.UserSelect;
type ServerDetailFragment = Prisma.UserGetPayload<{
select: typeof ServerDetailFragment;
}>;
const ServerDetail = ({ servers }: { servers: ServerDetailFragment }) => {
return (
<div>
<h2>Server Detail</h2>
<ul>
{servers.servers.map((server) => (
<li key={server.id}>{server.name}</li>
))}
</ul>
</div>
);
};
// ログインユーザーの詳細ページ (親コンポーネント)
import { db } from "@/lib/db";
import { Prisma } from "@prisma/client";
import { ServerDetail } from "./ServerDetail";
import { MemberDetail } from "./MemberDetail";
import type { ServerDetailFragment } from "./ServerDetail";
import type { MemberDetailFragment } from "./MemberDetail";
export const UserDetailFragment = {
userId: true,
name: true,
email: true,
imageUrl: true,
createdAt: true,
} satisfies Prisma.UserSelect;
export const UserDetail = async () => {
const loginUser = await currentLoginUser();
const user = await db.user.findUnique({
where: {
userId: loginUser.userId,
},
select: {
...UserDetailFragment,
...MemberDetailFragment,
...ServerDetailFragment,
},
});
if (!user) return <div>No Result.</div>;
return (
<div>
<div>
<div>{user?.userId} : {user?.name}</div>
<ServerDetail servers={user} />
<MemberDetail members={user} />
</div>
</div>
);
};
終わり
TypeScript4.9 にて追加されたsatisfies
演算子を使うことで、Prisma スキーマから生成される型情報に zod のスキーマが沿っている事を担保する事や、Fragment colocationライクなクエリの再利用を実装する事が出来ました。
記載している内容に何か間違いがありましたら、コメント頂ければと思います。
少しでもどなたかのお役に立てれば何よりです!
では明日のアドカレ記事にご期待ください!