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

如何高效生成带属性装饰器的灵活类,解决DTO重复编码维护问题

DTO校验类重复代码优化问题

最近在处理数据传输对象(DTO,本次场景中也可归为校验对象)的开发过程中,遇到了需要大量复制粘贴代码的困扰。
为简化问题描述,以通用的User类作为领域实体,演示DTO的实现逻辑。首先定义如下User对象接口:

interface User {
  name: string;
  age?: number;
}

业务校验规则

  • 新建用户时仅name属性为必填项
  • 编辑已有用户时所有属性均为选填
  • 可通过name查询用户
  • name属性仅允许包含字母字符
  • age属性必须为正整数
  • 实际业务中还有更多类似要求

现有实现方案及痛点

当使用class-validator库时,需要通过装饰器为对象添加校验规则。用于校验新建用户请求的User类实现如下:

class CreateUser implements User {
  @IsAlpha()
  name: string;

  @IsOptional()
  @IsPositive()
  @IsInt()
  age?: number;
}

完成新建用户的DTO后,还需要编辑用户的DTO:本质是要对CreateUser类做部分扩展,且name属性需要改为选填,同时添加@IsOptional()装饰器。
但直接继承CreateUser类会带来诸多不便,最突出的问题是如果重声明继承来的属性,父类属性上挂载的装饰器会丢失。该场景下一个可行方案是使用decorate-all库:

@DecorateAll(IsOptional, { deep: true });
class EditUser extends CreateUser implements Partial<User> {};

但该方案只能完成校验逻辑的适配,无法将name属性改为选填,需要手动将DTO实例强转为对应接口,这类强转属于常规操作,影响不大。

最后还需要实现按name查询用户的DTO:该DTO本质是继承CreateUser类但仅保留name属性。目前无法在类继承时实现Pick操作,只能通过复制粘贴代码实现:

class GetUserByName implements Pick<User, 'code'> {
  @IsAlpha()
  name: string;
}

上述实现逻辑并不复杂,但不难想象当业务规则较多时,这类代码会变得非常混乱,维护难度高且容易出错。

需求说明

是否存在更高效的实现方案?有没有办法可以配置化生成带装饰器的类?
理想中的实现方式如下:

interface DecoratedClassConfig<T> {
  [prop: keyof T]: PropertyDecorator[];
}

const decoratedUserConfig: DecoratedClassConfig<User> = {
  name: [IsAlpha],
  age: [IsOptional, IsPositive, IsInt],
};

class BaseUser implements User {
  name: string;
  age?: number;
}

class CreateUser extends DecorateClass(BaseUser, decoratedUserConfig) {}

const decoratedEditUserConfig = {
  ...decoratedUserConfig,
  user: [...decoratedUserConfig.user, IsOptional]
};
// Or just decorate the class with: @DecorateAll(IsOptional, { deep: true });
class EditUser extends DecorateClass(BaseUser, decoratedEditUserConfig) {}

const decoratedGetByUserNameConfig = {
  name: decoratedUserConfig.name,
};
class GetUserByName extends DecorateClass(BaseUser, decoratedGetByUserNameConfig) {}

解决方案

你构想的配置化实现方案完全可以落地,只需要自行实现一个通用的装饰器挂载工厂函数即可,核心逻辑是动态遍历配置项,为目标类的对应属性批量挂载装饰器:

type DecoratedClassConfig<T> = {
  [K in keyof T]?: PropertyDecorator[]
};

function DecorateClass<T extends new (...args: any[]) => any>(
  BaseCls: T,
  config: DecoratedClassConfig<InstanceType<T>>
): T {
  Object.entries(config).forEach(([prop, decorators]) => {
    decorators.forEach(decorator => decorator(BaseCls.prototype, prop));
  });
  return BaseCls;
}

针对Partial、Pick等TS类型映射需求,只需要在生成类时补充泛型类型断言即可,不需要额外手动强转实例:

// 生成编辑用户DTO,类型自动映射为Partial<User>
const EditUser = DecorateClass(BaseUser, {
  ...decoratedUserConfig,
  // 注:你原构想代码中的user字段为笔误,应为name字段
  name: [...decoratedUserConfig.name, IsOptional]
}) as new () => Partial<User>;

// 生成查询用户DTO,类型自动映射为Pick<User, 'name'>
const GetUserByName = DecorateClass(BaseUser, {
  name: decoratedUserConfig.name
// 注:你原实现中的Pick<User, 'code'>为笔误,应为'name'
}) as new () => Pick<User, 'name'>;

如果你不想自行实现工具函数,也可以直接使用@nestjs/mapped-types库,它原生提供了PartialType、PickType、OmitType等工具函数,可以直接基于已有的DTO类生成新的校验类,自动继承父类的所有装饰器规则,完美适配class-validator生态。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.09.28 03:45:04