You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.18 05:45:56