基于Prisma+MongoDB实现类型安全通用CRUD函数遇类型错误求助
通用Prisma CRUD函数的TypeScript类型错误解决方案
问题背景
使用Prisma搭配MongoDB开发服务,结合接口生成器与Zod Schema生成器,希望为所有模型实现通用的create/findMany/findUnique/update/delete CRUD函数,但遇到TypeScript错误:This expression is not callable.。此外,希望避免同时传入模型名的泛型和普通参数,但移除泛型会丢失createArgs的类型安全。
当前create函数实现:
import { Prisma, PrismaClient } from "@prisma/client"; import { ZodError } from "zod"; import { prismaClient } from ".."; type PrismaModelName = Uncapitalize<Prisma.ModelName>; type CreateArgs<T extends PrismaModelName> = T extends keyof PrismaClient ? Parameters<PrismaClient[T]["create"]>[0] : never; function getDelegate(model: PrismaModelName) { return prismaClient[model]; } async function createDocument<Model, ModelName extends PrismaModelName>( model: ModelName, createArgs: CreateArgs<ModelName>, createArgsSchema: Zod.Schema ): Promise<Model | { error: any }> { try { createArgsSchema.parse(createArgs); const delegate = getDelegate(model); const newDocument = await delegate.create(createArgs); return newDocument; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === "P2002") { return { error: "Document already exists." }; } } else if (error instanceof ZodError) { return { error: error.flatten() }; } return { error }; } }
完整错误信息:
This expression is not callable. Each member of the union type '(<T extends TeacherCreateArgs>(args: SelectSubset<T, TeacherCreateArgs<DefaultArgs>>) => Prisma__TeacherClient<GetFindResult<$TeacherPayload<DefaultArgs>, T, {}>, never, DefaultArgs>) | ... 8 more ... | (<T extends RoomCreateArgs>(args: SelectSubset<...>) => Prisma__RoomClient<...>)' has signatures, but none of those signatures are compatible with each other.ts(2349)
错误原因
getDelegate函数返回的是Prisma客户端所有模型代理的联合类型,不同模型的create方法签名存在差异,TypeScript无法确定具体调用哪个签名,因此抛出"无法调用"的错误。同时原函数需要手动传入两个泛型,导致冗余且容易出错。
解决方案
1. 优化类型定义与代理获取函数
给getDelegate添加泛型约束,让它根据传入的模型名返回具体的模型代理类型,而非联合类型。同时新增自动推断返回类型的工具类型,避免手动指定泛型。
2. 简化CRUD函数的泛型参数
通过模型名字符串自动推断参数类型和返回类型,无需手动传入额外泛型。
修改后的完整代码:
import { Prisma, PrismaClient } from "@prisma/client"; import { ZodError, ZodSchema } from "zod"; import { prismaClient } from ".."; // 模型名类型:自动将Prisma.ModelName转为小写 type PrismaModelName = Uncapitalize<Prisma.ModelName>; // 工具类型:获取指定模型的Create参数类型 type CreateArgs<T extends PrismaModelName> = Parameters<PrismaClient[T]["create"]>[0]; // 工具类型:获取指定模型的Create操作返回类型 type ModelCreateReturnType<T extends PrismaModelName> = Awaited<ReturnType<PrismaClient[T]["create"]>>; // 带泛型的代理获取函数:返回具体模型的代理 function getDelegate<T extends PrismaModelName>(model: T): PrismaClient[T] { return prismaClient[model] as PrismaClient[T]; } // 优化后的create通用函数 async function createDocument<T extends PrismaModelName>( model: T, createArgs: CreateArgs<T>, createArgsSchema: ZodSchema ): Promise<ModelCreateReturnType<T> | { error: any }> { try { createArgsSchema.parse(createArgs); const delegate = getDelegate(model); // TypeScript现在能正确识别delegate.create的具体签名 const newDocument = await delegate.create(createArgs); return newDocument; } catch (error) { if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error.code === "P2002") { return { error: "文档已存在。" }; } } else if (error instanceof ZodError) { return { error: error.flatten() }; } return { error }; } }
3. 使用示例(无需手动指定泛型)
// TypeScript自动根据"school"推断参数和返回类型 const newSchool = await createDocument( "school", { data: { name: "School 2", }, }, SchoolCreateArgsSchema );
4. 其他CRUD函数的实现思路
以findMany为例,复用相同的泛型约束逻辑:
// 工具类型:获取指定模型的FindMany参数类型 type FindManyArgs<T extends PrismaModelName> = Parameters<PrismaClient[T]["findMany"]>[0]; // 工具类型:获取指定模型的FindMany返回类型 type ModelFindManyReturnType<T extends PrismaModelName> = Awaited<ReturnType<PrismaClient[T]["findMany"]>>; async function findManyDocuments<T extends PrismaModelName>( model: T, findArgs: FindManyArgs<T>, schema?: ZodSchema ): Promise<ModelFindManyReturnType<T> | { error: any }> { try { schema?.parse(findArgs); const delegate = getDelegate(model); const documents = await delegate.findMany(findArgs); return documents; } catch (error) { // 复用create函数的错误处理逻辑 if (error instanceof Prisma.PrismaClientKnownRequestError) { // 按需添加特定错误码处理 } else if (error instanceof ZodError) { return { error: error.flatten() }; } return { error }; } }
内容的提问来源于stack exchange,提问作者shestakov-dev
相关产品推荐
相关产品推荐

