如何高效生成带属性装饰器的灵活类,解决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

