如何在TS单包中实现导入触发的条件模块扩展插件系统?
基于TypeScript实现单包插件系统的解决方案
尝试复刻Dayjs式条件模块扩展(有局限)
Dayjs的插件依赖TS的模块声明合并+静态解析特性,但纯TS项目中要实现「仅导入插件才生效类型扩展」,需要结合条件类型+运行时符号标记来规避未导入时的类型生效问题:
- 每个插件文件导出一个唯一的符号(如
export const __PLUGIN_ENABLED__ = Symbol()),用于标记插件已被导入。 - 在插件的类型声明文件中,通过条件类型判断该符号是否存在,仅当存在时才扩展主库的类型:
// plugin/aggregate.d.ts import type { MongoLibInstance } from '../main' import { __PLUGIN_ENABLED__ } from './aggregate' declare module '../main' { // 仅当插件被导入,__PLUGIN_ENABLED__存在时,才应用类型扩展 interface MongoLibInstance extends (typeof __PLUGIN_ENABLED__ extends never ? {} : { advancedAggregate: (pipeline: any[]) => Promise<any> }) {} } - 主库的
extend方法中,仅当插件被显式调用后才挂载对应方法,同时用类型守卫确保运行时的安全性。
注意:这种方式有明显局限——TS静态解析特性可能导致类型判断不够精准,若用户未导入插件但手动引用插件类型,仍会出现类型错误,且每个插件都需单独维护条件类型逻辑。
TS友好的替代插件模式(无需多包发布)
如果复刻Dayjs模式的成本过高,以下几种模式更适配TS的静态类型系统:
1. 显式类型注入模式
主库提供基础类型和扩展方法,插件导出自身的扩展类型与实现,用户手动合并类型,完全规避自动类型扩展的问题:
- 主库实现:
// main.ts export interface BaseMongoLib {} export class MongoLib implements BaseMongoLib { extend<T>(plugin: (instance: this) => T): this & T { Object.assign(this, plugin(this)) return this as this & T } } - 插件实现:
// plugin/aggregate.ts export interface AggregatePlugin { advancedAggregate: (pipeline: any[]) => Promise<any> } export function aggregatePlugin(instance: BaseMongoLib): AggregatePlugin { return { advancedAggregate: async (pipeline) => { // 具体实现逻辑 } } } // 提供辅助类型简化用户合并操作 export type WithAggregate<T> = T & AggregatePlugin - 用户使用:
优点:类型完全可控,未导入插件时不会自动扩展类型,主库原定义无报错;实现简单,无需复杂的模块声明。import { MongoLib } from 'my-mongo-lib' import { aggregatePlugin, WithAggregate } from 'my-mongo-lib/plugin/aggregate' const db = new MongoLib().extend(aggregatePlugin) as WithAggregate<MongoLib> // 此时db.advancedAggregate拥有完整类型提示
缺点:用户需手动合并类型,但可通过辅助类型简化操作。
2. 插件注册式类型扩展
主库定义全局类型注册表,插件向注册表注入自身类型,用户必须显式导入插件并调用extend启用:
- 主库实现:
// main.ts declare global { // 全局插件类型注册表,默认为空 interface MongoLibPlugins {} } export interface BaseMongoLib {} export class MongoLib implements BaseMongoLib { extend<K extends keyof MongoLibPlugins>(pluginKey: K): this & MongoLibPlugins[K] { // 动态加载插件(需确保TS配置支持动态导入) const plugin = require(`./plugin/${pluginKey}`).default Object.assign(this, plugin(this)) return this as this & MongoLibPlugins[K] } } - 插件实现:
// plugin/aggregate.ts declare global { interface MongoLibPlugins { aggregate: { advancedAggregate: (pipeline: any[]) => Promise<any> } } } export default function aggregatePlugin(instance: BaseMongoLib) { return { advancedAggregate: async (pipeline) => { // 具体实现逻辑 } } } - 用户使用:
优点:用户只需导入插件并调用import { MongoLib } from 'my-mongo-lib' // 必须导入插件,否则注册表中无aggregate类型 import 'my-mongo-lib/plugin/aggregate' const db = new MongoLib().extend('aggregate') // db.advancedAggregate自动获得类型提示extend,类型自动合并;未导入插件时,extend不会提示对应的插件key,避免误用。
缺点:依赖全局接口扩展,严格模式下可能需配置skipLibCheck确保类型兼容。
3. 装饰器模式(类扩展场景)
通过类装饰器实现插件扩展,用户用装饰器包裹主类即可启用插件,完全符合TS的类扩展特性:
- 主库实现:
// main.ts export class MongoLib { // 基础方法实现 } - 插件实现:
// plugin/aggregate.ts export function withAggregate<T extends typeof MongoLib>(Base: T) { return class extends Base { advancedAggregate(pipeline: any[]): Promise<any> { // 具体实现逻辑 } } } - 用户使用:
优点:类型安全,插件逻辑封装性好,用户使用直观;支持多插件叠加(嵌套装饰器)。import { MongoLib } from 'my-mongo-lib' import { withAggregate } from 'my-mongo-lib/plugin/aggregate' const EnhancedMongoLib = withAggregate(MongoLib) const db = new EnhancedMongoLib() // db.advancedAggregate拥有完整类型提示
缺点:仅适用于类扩展场景,无法直接扩展静态成员(需额外处理)。
内容的提问来源于stack exchange,提问作者strblr
相关产品推荐
相关产品推荐

