在使用内部模块的应用中如何正确引用模块类型?
问题背景
我在monorepo架构下开发一个扩展NestJS-Prisma功能的内部模块myPrismaModule,项目结构如下:
- apps |_serviceA |_node_modules |_myPrismaModule (安装了内部模块的构建版本) |_src |_generated |_client |_default.js (存放当前服务的Prisma类型) - internal |_myPrismaModule
由于各业务服务的Prisma Schema不同,对应的PrismaClient和Prisma类型也存在差异。原本直接从@prisma/client导入类型的方式在内部模块构建后无法正确引用业务服务的具体类型,于是尝试改成动态导入的方式,结果出现Cannot find namespace 'Prisma'错误;添加@ts-ignore后,业务侧访问prismaService.banner.findMany时,banner被识别为any类型,完全丢失了类型提示。
代码变更对比:
原代码
import {Prisma, PrismaClient} from "@prisma/client" @Injectable() export class PrismaService extends PrismaClient<Prisma.PrismaClientOptions, 'query' | 'info' | 'warn' | 'error'> implements OnModuleInit{...}
修改后代码
async function getPrismaClientType() { const {Prisma, PrismaClient} = await import( "~~absolutePath~~/src/apps/serviceA/src/generated/client/default.js'" ); return PrismaClient as PrismaClient< Prisma.PrismaClientOptions, 'query' | 'info' | 'warn' | 'error' >; } @Injectable() export class PrismaService extends (await getPrismaClientType()) implements OnModuleInit{...}
问题分析
动态导入的方式在TypeScript编译阶段无法获取到具体类型:
extends关键字后面不支持异步表达式,编译时无法解析await getPrismaClientType()的结果类型;- 动态导入的
Prisma命名空间在编译阶段不可见,导致类型断言失效; - 最终业务侧只能拿到
any类型,完全丢失了Prisma生成的模型类型提示。
解决方案
方案一:泛型+依赖注入实现类型解耦
让内部模块的PrismaService变成泛型类,由业务服务自行传入对应的Prisma类型,彻底解耦内部模块与业务侧的Schema依赖。
内部模块代码
import { Injectable, OnModuleInit, Inject } from '@nestjs/common'; // 定义泛型接口,约束业务侧需要提供的Prisma类型 export interface PrmaTypeBundle<TClient, TOptions, TEventTypes> { PrismaClient: new (options?: TOptions) => TClient; Prisma: { PrismaClientOptions: TOptions; }; } // 注入令牌,用于业务侧提供具体的类型实现 export const PRISMA_TYPE_BUNDLE = Symbol('PRISMA_TYPE_BUNDLE'); @Injectable() export class PrismaService<TClient, TOptions, TEventTypes> extends PrmaTypeBundle<TClient, TOptions, TEventTypes>['PrismaClient'] implements OnModuleInit { constructor( @Inject(PRISMA_TYPE_BUNDLE) private readonly typeBundle: PrmaTypeBundle<TClient, TOptions, TEventTypes> ) { super(); // 调用业务侧PrismaClient的构造函数 } async onModuleInit() { await this.$connect(); // 这里添加你的扩展逻辑,比如全局拦截器、日志处理等 } // 通用扩展方法,自动继承业务侧的PrismaClient类型 async softDelete(model: keyof TClient, where: any) { return (this as TClient)[model].update({ where, data: { deletedAt: new Date() }, }); } }
业务服务(serviceA)代码
import { Module } from '@nestjs/common'; import { PrismaService as InternalPrismaService, PRISMA_TYPE_BUNDLE, PrmaTypeBundle } from 'myPrismaModule'; import { Prisma, PrismaClient } from './generated/client'; // 提供当前服务的Prisma类型实现 const localPrismaBundle: PrmaTypeBundle< PrismaClient, Prisma.PrismaClientOptions, 'query' | 'info' | 'warn' | 'error' > = { PrismaClient, Prisma, }; @Module({ providers: [ { provide: PRISMA_TYPE_BUNDLE, useValue: localPrismaBundle, }, InternalPrismaService, ], exports: [InternalPrismaService], }) export class PrismaModule {}
这种方式下,业务侧的prismaService.banner.findMany会保留完整的类型提示,内部模块也能复用扩展逻辑。
方案二:将扩展逻辑抽离为装饰器/工具类
不让内部模块直接继承PrismaClient,而是把扩展逻辑做成可复用的装饰器或工具函数,由业务侧的PrismaService继承自身的PrismaClient后应用扩展。
内部模块代码
import { OnModuleInit } from '@nestjs/common'; export function withPrismaExtensions<T extends new (...args: any[]) => any>(Base: T) { return class extends Base implements OnModuleInit { async onModuleInit() { await this.$connect(); // 添加通用扩展逻辑,比如日志拦截 this.$use(async (params, next) => { console.log(`Query: ${params.model}.${params.action}`); return next(params); }); } // 自定义扩展方法 async batchFindMany(models: Array<{ model: keyof this; where: any }>) { return Promise.all( models.map(({ model, where }) => this[model].findMany(where)) ); } }; }
业务服务(serviceA)代码
import { Injectable } from '@nestjs/common'; import { Prisma, PrismaClient } from './generated/client'; import { withPrismaExtensions } from 'myPrismaModule'; @Injectable() export class PrismaService extends withPrismaExtensions( PrismaClient<Prisma.PrismaClientOptions, 'query' | 'info' | 'warn' | 'error'> ) {}
这种方式最直接,业务侧完全保留自己的Prisma类型,同时复用内部模块的扩展能力。
方案三:Monorepo路径别名配置(耦合性较高)
如果你的monorepo结构固定,可以通过TypeScript路径别名让内部模块直接引用业务侧的Prisma生成文件:
在内部模块的tsconfig.json中添加路径映射:
{ "compilerOptions": { "paths": { "@prisma/client": ["../apps/serviceA/src/generated/client"] } } }
然后回到最初的导入方式,但这种方式只适合单服务场景,多服务时需要为每个服务单独配置,耦合性较高。
推荐方案
优先选择方案一或方案二,两者都能很好地解耦内部模块与业务侧的Schema依赖,同时保留完整的类型提示。方案一更适合需要在内部模块中直接操作PrismaClient的场景,方案二则更轻量化,适合仅添加扩展逻辑的需求。
内容的提问来源于stack exchange,提问作者Justin Seo

