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

如何减少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的类:

  1. 先定义数据接口:
// student.model.ts
export interface Student {
  id: string;
  firstName: string;
  middleName: string;
  lastName: string;
  enrolledOn?: Date;
}
  1. 编写生成脚本:
// 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('数据类生成完成'));
  1. 运行脚本生成类,之后只需维护接口即可。
    优点:完全自定义,无运行时依赖;缺点:需要维护生成脚本,适合多个数据类的大型项目。

思路转变:TypeScript优先的思考方式

TypeScript更偏向于对象优先而非类优先,纯数据场景下,用接口+对象+纯函数的组合比类更简洁高效。类更适合需要封装行为(方法)的场景,而非单纯存储数据。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 14:27:17