如何为Prisma findFirst编写带可选Include的类型安全包装函数?
类型安全封装Prisma关联查询的Nest.js服务方案
问题背景
我用Prisma操作数据库,想在Nest.js服务里封装数据库访问逻辑。Prisma自带完善的类型检查机制,比如关联关系必须显式通过include指定才能访问:
// 编译报错:groups未被加载,无法访问 const user1 = db.user.findFirst({ where: { id: '1' } }); console.log(user1.groups); // 合法:显式声明include关联后可正常访问 const user2 = db.user.findFirst({ where: { id: '1' }, include: { groups: true } }); console.log(user2.groups);
但封装findFirst时,可选的include参数无法让Prisma正确推导返回类型:
public async getUserById<TInclude extends Prisma.UserInclude>( id: string, include?: TInclude ) { return db.user.findFirst({ where: { id /* 其他查询条件 */ }, include: include, }); }
调用时即使传入include,返回类型仍为Promise<User & {} | null>,因为Prisma会基于TInclude | undefined推导类型,忽略了本次调用中include实际存在的情况。
试过拆成两个独立函数(无include、必传include),但写法不够简洁;也试过函数重载,但需要手动编写复杂的返回类型,不想重复Prisma的内置类型逻辑。
最优解决方案
方案1:给include设置默认空对象(推荐)
把include的可选性改成带默认值的必填参数,让TypeScript能根据传入的实际值精准推导返回类型:
public async getUserById<TInclude extends Prisma.UserInclude>( id: string, include: TInclude = {} as TInclude ) { return db.user.findFirst({ where: { id /* 其他查询条件 */ }, include: include, }); }
调用方式完全兼容原有习惯:
- 不传入include:
getUserById('1'),返回Promise<User | null> - 传入include:
getUserById('1', { groups: true }),返回Promise<(User & { groups: Group[] }) | null>
方案2:利用Prisma内置类型工具+条件类型
如果要保留参数的可选性,可借助Prisma的Prisma.UserGetPayload类型工具,结合条件类型手动推导返回类型:
import type { Prisma } from '@prisma/client'; type UserWithInclude<TInclude extends Prisma.UserInclude | undefined> = TInclude extends undefined ? Prisma.UserGetPayload<{}> : Prisma.UserGetPayload<{ include: TInclude }>; public async getUserById<TInclude extends Prisma.UserInclude | undefined>( id: string, include?: TInclude ): Promise<UserWithInclude<TInclude> | null> { return db.user.findFirst({ where: { id /* 其他查询条件 */ }, include: include, }) as Promise<UserWithInclude<TInclude> | null>; }
该方案保留了include的可选性,同时让返回类型与传入参数正确关联。
方案3:函数重载+复用Prisma返回类型
如果偏好函数重载写法,可利用Prisma的FindFirstUserArgs和UserGetPayload复用内置类型,避免手动维护复杂类型结构:
import type { Prisma } from '@prisma/client'; // 重载1:无include参数 public async getUserById(id: string): Promise<Prisma.UserGetPayload<{}> | null>; // 重载2:带include参数 public async getUserById<TInclude extends Prisma.UserInclude>( id: string, include: TInclude ): Promise<Prisma.UserGetPayload<{ include: TInclude }> | null>; // 通用实现 public async getUserById<TInclude extends Prisma.UserInclude | undefined>( id: string, include?: TInclude ) { return db.user.findFirst({ where: { id /* 其他查询条件 */ }, include: include, }); }
这种方式既保留了简洁的调用体验,又完全复用了Prisma的类型逻辑。
内容的提问来源于stack exchange,提问作者Yavanosta
相关产品推荐
相关产品推荐

