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

如何解决NestJS存储包的类型安全问题:磁盘提示与参数兼容

NestJS存储包全类型安全实现方案

问题背景

开发NestJS存储工具包,目标是支持多存储提供商(Local、S3等)并实现全类型安全,当前遇到两个核心问题:

  • 创建驱动实例时,TypeScript报错Argument of type 'LocalOptions | S3Options' is not assignable to parameter of type 'LocalOptions & S3Options'
  • 调用StorageService.disk()时,IDE无法自动提示已配置的磁盘名称(如invoices、images)

当前代码实现

local.driver.ts

interface LocalOptions {
  path: string;
}

export class LocalDriver {
  constructor(private options: LocalOptions) {}

  get() {}
}

s3.driver.ts

interface S3Options {
  key: string;
  secret: string;
  region: string;
  bucket: string;
}

export class S3Driver {
  constructor(private options: S3Options) {}

  get() {}
}

storage.service.ts(原代码)

const drivers = {
  local: LocalDriver,
  s3: S3Driver,
};

type DriverTypes = keyof typeof drivers;
type Drivers = typeof drivers[DriverTypes];

type Disk = {
  [Driver in DriverTypes]: {
    type: Driver;
    properties: ConstructorParameters<typeof drivers[Driver]>[0];
  };
}[DriverTypes];

export type StorageOptions = {
  disks: {
    [disk: string]: Disk;
  };
};

@Injectable()
export class StorageService {
  private static diskDrivers: { [disk: string]: Drivers };
  private static options: StorageOptions;

  constructor(@Inject(STORAGE_OPTIONS) options: StorageOptions) {
    StorageService.options = options;
  }

  static disk(disk: keyof typeof StorageService.options.disks) {
    return StorageService.getDriver(disk);
  }

  private static getDriver(
    disk: keyof typeof StorageService.options.disks,
  ): Drivers {
    const diskDriver = StorageService.diskDrivers[disk];
    if (diskDriver) return diskDriver;
    const driver = StorageService.newDriver(disk);
    StorageService.diskDrivers[disk] = driver;
    return driver;
  }

  private static newDriver(
    disk: keyof typeof StorageService.options.disks,
  ): Drivers {
    const driverConfig = StorageService.options.disks[disk];
    const Driver = drivers[driverConfig.type];
    return new Driver(driverConfig.properties);
                                    // ^ 类型不匹配报错位置
  }
}

app.module.ts(注册代码)

@Module({
  imports: [
    StorageModule.register({
      disks: {
        invoices: {
          type: 's3',
          properties: {
            region: process.env.AWS_REGION,
            key: process.env.AWS_KEY,
            secret: process.env.AWS_SECRET,
            bucket: 'invoices',
          },
        },
        images: {
          type: 'local',
          properties: {
            path: '/images',
          },
        },
      },
    }),
  ],
})
export class AppModule {}

解决方案

核心思路

通过泛型约束绑定磁盘名称、驱动类型、配置参数三者的关联关系,让TypeScript能追踪到具体的类型信息,同时让StorageService与注册时的配置类型绑定,实现IDE自动提示。

修改后的完整代码

storage.service.ts

import { Inject, Injectable } from '@nestjs/common';
import { LocalDriver, LocalOptions } from './local.driver';
import { S3Driver, S3Options } from './s3.driver';

const drivers = {
  local: LocalDriver,
  s3: S3Driver,
};

type DriverMapping = typeof drivers;
type DriverType = keyof DriverMapping;

// 重构Disk类型,确保type与properties严格匹配
type Disk<DT extends DriverType = DriverType> = DT extends DriverType 
  ? {
      type: DT;
      properties: ConstructorParameters<DriverMapping[DT]>[0];
    }
  : never;

// 泛型StorageOptions,保留具体磁盘名称的类型信息
export type StorageOptions<DiskNames extends string = string> = {
  disks: {
    [Name in DiskNames]: Disk;
  };
};

@Injectable()
export class StorageService<DiskNames extends string> {
  private static diskDrivers: Record<string, InstanceType<DriverMapping[DriverType]>> = {};
  private static options: StorageOptions;

  constructor(@Inject('STORAGE_OPTIONS') options: StorageOptions<DiskNames>) {
    StorageService.options = options;
  }

  // 泛型disk方法,返回对应磁盘的具体驱动类型
  static disk<Name extends keyof typeof StorageService.options.disks>(
    disk: Name
  ): InstanceType<DriverMapping[(typeof StorageService.options.disks)[Name]['type']]> {
    return StorageService.getDriver(disk) as InstanceType<DriverMapping[(typeof StorageService.options.disks)[Name]['type']]>;
  }

  private static getDriver(
    disk: keyof typeof StorageService.options.disks
  ): InstanceType<DriverMapping[DriverType]> {
    const existingDriver = StorageService.diskDrivers[disk];
    if (existingDriver) return existingDriver;
    const newDriver = StorageService.newDriver(disk);
    StorageService.diskDrivers[disk] = newDriver;
    return newDriver;
  }

  // 泛型newDriver方法,确保配置与驱动类型匹配
  private static newDriver<Name extends keyof typeof StorageService.options.disks>(
    disk: Name
  ): InstanceType<DriverMapping[(typeof StorageService.options.disks)[Name]['type']]> {
    const config = StorageService.options.disks[disk];
    const DriverClass = drivers[config.type];
    // 类型断言:通过泛型已确保类型匹配
    return new DriverClass(config.properties as ConstructorParameters<typeof DriverClass>[0]);
  }
}

storage.module.ts(新增/修改)

import { DynamicModule, Module } from '@nestjs/common';
import { StorageOptions, StorageService } from './storage.service';

@Module({})
export class StorageModule {
  static register<DiskNames extends string>(options: StorageOptions<DiskNames>): DynamicModule {
    return {
      module: StorageModule,
      providers: [
        {
          provide: 'STORAGE_OPTIONS',
          useValue: options,
        },
        // 绑定具体磁盘类型的StorageService实例
        {
          provide: StorageService,
          useClass: StorageService<DiskNames>,
        },
      ],
      exports: [StorageService],
    };
  }
}

效果验证

修改完成后:

  1. 调用StorageService.disk('')时,IDE会自动提示可选的磁盘名称invoices和images
  2. 创建驱动实例的类型报错消失,且disk('invoices')返回S3Driver类型,disk('images')返回LocalDriver类型,调用驱动方法时会有正确的类型提示

内容的提问来源于stack exchange,提问作者samsepi

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 21:05:18