如何减少TypeScript数据类的样板代码?实现命名参数与copyWith
解决TypeScript数据类样板代码过多的方案
作为有Kotlin/Java、Dart等语言经验的开发者,你会发现TypeScript没有内置的数据类语法,导致定义带copyWith和命名参数的数据结构时样板代码冗余。以下是几种实用的解决方案:
1. 轻量方案:接口+纯函数(推荐纯数据场景)
放弃类,用TypeScript接口定义数据结构,配合工厂函数和通用copyWith函数,完全符合JS/TS的函数式风格,几乎无样板:
// 仅定义一次数据结构 interface Student { id: string; firstName: string; middleName: string; lastName: string; enrolledOn?: Date; } // 命名参数构造(工厂函数) const createStudent = (props: Student): Student => ({ ...props }); // 通用copyWith,可复用所有类型 function copyWith<T>(obj: T, partial: Partial<T>): T { return { ...obj, ...partial }; } // 使用示例 const student = createStudent({ id: "s1", firstName: "Alice", middleName: "Marie", lastName: "Brown", }); const updatedStudent = copyWith(student, { enrolledOn: new Date() });
优点:无冗余代码,类型安全,无需额外依赖;缺点:没有类的实例方法,适合纯数据存储场景。
2. 简化类写法:复用接口类型
如果必须使用类,可以通过接口复用类型,减少字段重复定义:
interface StudentProps { id: string; firstName: string; middleName: string; lastName: string; enrolledOn?: Date; } class Student implements StudentProps { // 接口自动约束类型,无需重复标注 id: string; firstName: string; middleName: string; lastName: string; enrolledOn?: Date; constructor(props: StudentProps) { // 用Object.assign简化赋值,避免逐个this.xxx = props.xxx Object.assign(this, props); } copyWith(partial: Partial<StudentProps>): Student { return new Student({ ...this, ...partial }); } }
优点:字段仅定义2次(接口+类属性),构造器和copyWith无需重复写类型;缺点:仍需手动声明类属性(TypeScript暂不支持自动实现接口属性)。
3. 装饰器库:类似Dart Freezed的体验
使用第三方装饰器库自动生成copyWith、命名构造等方法,接近Kotlin数据类的开发体验,比如freezed-ts:
首先安装依赖,配置tsconfig.json开启experimentalDecorators: true和emitDecoratorMetadata: true,然后编写代码:
import { freezed, Freezed } from 'freezed-ts'; @freezed class Student extends Freezed { constructor( public readonly id: string, public readonly firstName: string, public readonly middleName: string, public readonly lastName: string, public readonly enrolledOn?: Date, ) { super(); } } // 命名参数构造(库自动生成的工厂方法) const student = Student.create({ id: "s1", firstName: "Bob", middleName: "Lee", lastName: "Davis", }); // 自动生成的copyWith方法 const updatedStudent = student.copyWith({ enrolledOn: new Date() });
优点:仅需定义一次字段,自动生成样板代码;缺点:需要引入第三方依赖,依赖装饰器特性。
4. 代码生成:自定义脚本自动生成类
如果不想依赖第三方库,可以用ts-morph等工具编写简单的代码生成脚本,根据接口自动生成带copyWith的类:
- 先定义数据接口:
// student.model.ts export interface Student { id: string; firstName: string; middleName: string; lastName: string; enrolledOn?: Date; }
- 编写生成脚本:
// generate-data-class.ts import { Project, InterfaceDeclaration } from 'ts-morph'; const project = new Project(); const sourceFile = project.addSourceFileAtPath('./student.model.ts'); const studentInterface = sourceFile.getInterface('Student') as InterfaceDeclaration; const className = studentInterface.getName(); const properties = studentInterface.getProperties().map(p => p.getName()); // 生成类代码 const classCode = ` export class ${className} { constructor(props: ${className}) { ${properties.map(prop => `this.${prop} = props.${prop};`).join('\n ')} } copyWith(partial: Partial<${className}>): ${className} { return new ${className}({ ...this, ...partial }); } } `; sourceFile.addStatements(classCode); sourceFile.save().then(() => console.log('数据类生成完成'));
- 运行脚本生成类,之后只需维护接口即可。
优点:完全自定义,无运行时依赖;缺点:需要维护生成脚本,适合多个数据类的大型项目。
思路转变:TypeScript优先的思考方式
TypeScript更偏向于对象优先而非类优先,纯数据场景下,用接口+对象+纯函数的组合比类更简洁高效。类更适合需要封装行为(方法)的场景,而非单纯存储数据。
内容的提问来源于stack exchange,提问作者or9ob
相关产品推荐
相关产品推荐

