📚 TypeScript 教程系列

  1. 入门与配置
  2. 基础类型与变量声明
  3. 函数
  4. 流程控制与运算符
  5. 集合类型
  6. 异步编程与错误处理
  7. 接口与类
  8. 泛型与类型组合
  9. 高级类型
  10. 模块、装饰器与工程化(本文)

⚠️ 来源声明:本文内容参考自 菜鸟教程 TypeScript 教程,仅供学习交流,版权归原作者所有。
本篇是 TypeScript 教程的收官之作,聚焦于工程化层面的核心能力:从命名空间与模块系统的组织方式,到声明文件如何桥接 JavaScript 生态,再到装饰器的元编程能力与路径映射、项目引用、Monorepo 等大型项目配置。最后覆盖从 JS 迁移、单元测试、设计模式到性能优化与综合类型设计,帮助你把 TypeScript 真正用进真实项目。

命名空间

命名空间的主要目标是解决命名冲突问题。就像一个班上有两个叫"小明"的学生,需要通过姓氏(王小明、李小明)等额外信息来区分他们。命名空间定义了标识符的可见范围,一个标识符可在多个命名空间中定义,在不同命名空间中同名标识符的含义互不干扰。

语法定义

TypeScript 使用 namespace 关键字来定义命名空间,基本格式如下:

1
2
3
4
namespace SomeNameSpaceName {
export interface ISomeInterfaceName { }
export class SomeClassName { }
}

上面创建了一个名为 SomeNameSpaceName 的命名空间。要让外部能访问其中的类和接口,需要加上 export 关键字。在另一个命名空间中调用的语法为:

1
SomeNameSpaceName.SomeClassName;

如果命名空间定义在单独的 .ts 文件中,则需要用三斜杠指令引用它:

1
/// <reference path = "SomeFileName.ts" />

多文件实例

IShape.ts 文件:

1
2
3
4
5
namespace Drawing {
export interface IShape {
draw();
}
}

Circle.ts 文件:

1
2
3
4
5
6
7
8
/// <reference path = "IShape.ts" />
namespace Drawing {
export class Circle implements IShape {
public draw() {
console.log("Circle is drawn");
}
}
}

Triangle.ts 文件:

1
2
3
4
5
6
7
8
/// <reference path = "IShape.ts" />
namespace Drawing {
export class Triangle implements IShape {
public draw() {
console.log("Triangle is drawn");
}
}
}

TestShape.ts 文件:

1
2
3
4
5
6
7
8
/// <reference path = "IShape.ts" />
/// <reference path = "Circle.ts" />
/// <reference path = "Triangle.ts" />
function drawAllShapes(shape: Drawing.IShape) {
shape.draw();
}
drawAllShapes(new Drawing.Circle());
drawAllShapes(new Drawing.Triangle());

使用 tsc --out app.js TestShape.ts 编译后,运行 node app.js 输出:

1
2
Circle is drawn
Triangle is drawn

嵌套命名空间

命名空间支持嵌套,即可以将命名空间定义在另外一个命名空间里头。

1
2
3
4
5
namespace namespace_name1 {
export namespace namespace_name2 {
export class class_name { }
}
}

成员访问使用点号 . 实现:

Invoice.ts 文件:

1
2
3
4
5
6
7
8
9
namespace Runoob {
export namespace invoiceApp {
export class Invoice {
public calculateDiscount(price: number) {
return price * .40;
}
}
}
}

InvoiceTest.ts 文件:

1
2
3
/// <reference path = "Invoice.ts" />
var invoice = new Runoob.invoiceApp.Invoice();
console.log(invoice.calculateDiscount(500));

编译命令:tsc --out app.js InvoiceTest.ts,运行输出:

1
200

模块

模块系统是现代 TypeScript 开发的基础。TypeScript 完全支持 ES Module 语法,并提供了丰富的模块解析策略。通过模块系统,可以将代码分割成可重用的单元,实现代码组织和复用。

为什么需要模块系统

随着项目规模增长,代码量会越来越大。将代码分散到多个文件中,通过模块组织,可以提高代码的可维护性和可复用性。模块系统让每个文件都有自己的作用域,避免全局变量污染。

模块是包含导出和导入语句的 TypeScript 文件。通过 export 导出内容,通过 import 导入内容。

模块导出

使用 export 关键字可以将变量、函数、类、接口等导出供其他模块使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// user.ts

// 导出变量
export var name = "Alice";
export const age = 25;

// 导出函数
export function greet(message: string): string {
return "Hello, " + message;
}

// 导出类
export class User {
constructor(public name: string) {}

introduce(): string {
return "I am " + this.name;
}
}

// 导出接口(接口在编译后会消失,仅用于类型检查)
export interface Config {
host: string;
port: number;
}

// 批量导出:重命名导出
export { name as userName, age as userAge };

接口和类型在编译后的 JavaScript 中不会产生实际代码,它们仅用于 TypeScript 的类型检查。

模块导入

使用 import 关键字从其他模块导入导出的内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// main.ts

// 命名导入:从模块中导入指定的内容
import { name, age, greet } from "./user";

// 默认导入:导入模块的默认导出
import User from "./user";

// 全部导入:将模块所有导出放入一个对象
import * as UserModule from "./user";

// 重命名导入:避免命名冲突
import { greet as sayHello } from "./user";

// 使用导入的内容
console.log(greet("World"));
console.log(sayHello("TypeScript"));

运行结果:

1
2
Hello, World
Hello, TypeScript

导入路径可以是相对路径(如 ./user)或绝对路径(如 @/utils)。

默认导出

每个模块可以有一个默认导出。默认导出在导入时不需要使用花括号,且可以取任意名字。

1
2
3
4
5
6
7
8
9
10
11
// math.ts

// 默认导出:一个模块只能有一个默认导出
export default function add(a: number, b: number): number {
return a + b;
}

// 可以和其他导出混合使用
export function multiply(a: number, b: number): number {
return a * b;
}
1
2
3
4
5
6
7
8
9
10
// main.ts

// 导入默认导出:可以取任意名字
import add from "./math";

// 导入命名导出:需要使用花括号
import { multiply } from "./math";

console.log("加法: " + add(2, 3));
console.log("乘法: " + multiply(4, 5));

运行结果:

1
2
加法: 5
乘法: 20

对于工具函数、类等主要导出内容使用默认导出,对于辅助函数、接口等使用命名导出。

重新导出

重新导出(Re-export)用于聚合多个模块的内容,或将一个模块的导出暴露给另一个模块。

1
2
3
4
5
6
7
8
9
10
// index.ts 聚合模块

// 从其他模块重新导出指定内容
export { name, age } from "./user";

// 重新导出默认导出(需要重命名)
export { default as User } from "./user";

// 重新导出所有内容
export * from "./math";

使用 index.ts 作为入口文件,集中导出子模块的内容,方便统一导入。

动态导入

动态导入(Dynamic Import)使用 import() 语法,可以在运行时按需加载模块。这对于代码分割、懒加载非常有用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 动态导入 - 懒加载
// import() 返回一个 Promise
async function loadMath() {
// 动态导入模块,只有执行到这里才会加载
var math = await import("./math");

// math.default 是默认导出的函数
console.log("动态加法: " + math.default(1, 2));
}

// 调用懒加载函数
loadMath();

// 条件导入:根据条件动态加载不同模块
async function loadFeature(enable: boolean) {
if (enable) {
// 只有条件满足时才加载模块
var feature = await import("./feature");
feature.run();
}
}

// 根据条件加载
loadFeature(true);

运行结果:

1
动态加法: 3

动态导入可以实现代码分割,只在需要时加载额外的代码,减少初始加载时间。

模块解析策略

TypeScript 提供了多种模块解析策略,用于查找导入的模块,可以在 tsconfig.json 中配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"compilerOptions": {
// Node 解析策略:遵循 Node.js 的模块解析规则
"moduleResolution": "node",

// 经典解析策略:TypeScript 早期版本使用的策略
"moduleResolution": "classic",

// base URL:设置基础路径
// 所有非相对路径导入都基于此路径解析
"baseUrl": "./src",

// 路径映射:为导入路径设置别名
"paths": {
"@/*": ["./*"],
"@components/*": ["./components/*"]
}
}
}

新项目推荐使用 Node 解析策略,它是目前最常用的方式。

命名空间与模块的选择

命名空间和模块都能组织代码,但适用场景不同:

  • 命名空间:基于全局对象,适合遗留项目或简单的脚本组织,通过三斜杠指令引用文件。
  • 模块:基于文件作用域,是现代标准,支持 Tree Shaking 和按需加载,推荐用于所有新项目。

在现代 TypeScript 开发中,应优先使用模块(ES Module)。命名空间主要在维护旧代码或与某些全局库交互时使用。

声明文件

声明文件充当着 TypeScript 与 JavaScript 库之间的桥梁,它告诉 TypeScript 一个 JavaScript 库暴露了哪些功能、参数是什么类型、返回值是什么类型。

先从一个问题说起

假设在 TypeScript 项目中引入第三方 JavaScript 库(如 jQuery),代码可能写成:

1
2
3
$('#foo');
// 或
jQuery('#foo');

这在纯 JavaScript 中没问题,但在 TypeScript 文件中会报错:

1
2
jQuery('#foo');
// index.ts(1,1): error TS2304: Cannot find name 'jQuery'.

报错原因是 TypeScript 不认识 $jQuery。TypeScript 在编译阶段就需要知道每个变量和函数的类型,而 jQuery 作为纯 JavaScript 库不包含任何类型信息。

快速修复:declare 关键字

最简单的方式是用 declare 关键字手动告知 TypeScript 某个变量的存在及其类型:

1
2
declare var jQuery: (selector: string) => any;
jQuery('#foo');

这段代码的含义是:声明变量 jQuery 为一个函数,接收 string 类型参数,返回 any 类型。declare 关键字声明的类型只在编译阶段起作用,编译后的 JavaScript 代码中会被完全删除,不会影响运行时的行为。编译后的 JavaScript 代码为:

1
jQuery('#foo');

declare 只能解决单个文件的临时问题。对于包含许多方法和类的库,在每个文件中手写 declare 不现实,因此需要声明文件。

声明文件:一劳永逸的方案

声明文件将所有 declare 声明集中放到一个独立文件中,项目中的任何 TypeScript 文件都可以引用它。

文件命名规范

声明文件统一以 .d.ts 为后缀,d 代表 declaration(声明)。例如 runoob.d.ts

基本语法

声明一个模块的语法如下:

1
2
declare module Module_Name {
}

在 TypeScript 文件中通过三斜线指令引入声明文件:

1
/// <reference path = "runoob.d.ts" />

三斜线指令是 TypeScript 特有的语法,用于告知编译器在编译时需要包含指定的声明文件。很多流行第三方库(如 jQuery、Lodash)的声明文件已经由社区维护好了,存放在 DefinitelyTyped 项目中,通过 npm 安装对应的 @types/xxx 包即可使用。

完整实例:从零创建声明文件

整个流程涉及以下文件:

文件作用
CalcThirdPartyJsLib.js第三方 JavaScript 库(纯 JS,无类型信息)
Calc.d.ts声明文件(手动编写,描述库的类型)
CalcTest.tsTypeScript 业务代码(引用声明文件,调用库)
CalcTest.js编译产物(tsc 编译后的 JS 文件)
runoob.html浏览器中运行的最终页面

第一步:创建第三方 JavaScript 库

假设有一个第三方库,提供累加求和功能,使用命名空间 Runoob 组织代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// CalcThirdPartyJsLib.js
// 这是一个模拟的第三方 JavaScript 库

// 声明 Runoob 命名空间变量(如果不存在则创建空对象)
var Runoob;

// 使用立即执行函数(IIFE)封装代码,避免内部变量泄漏到全局作用域
(function(Runoob) {
// Calc 构造函数,用于创建计算器对象
var Calc = (function () {
function Calc() {
// 当前无需初始化参数,保留构造函数以便后续扩展
}
})

// doSum 方法:计算从 0 到 limit 的所有整数之和
// limit(必填):累加的上限值,包含该值本身
// 示例:limit=10 时,计算 0+1+2+...+10 = 55
Calc.prototype.doSum = function (limit) {
var sum = 0;

for (var i = 0; i <= limit; i++) {
sum = sum + i;
}
return sum;
}

// 将 Calc 构造函数挂载到 Runoob 命名空间下
Runoob.Calc = Calc;
return Calc;
})(Runoob || (Runoob = {}));

// 库内部自测代码
var test = new Runoob.Calc();

这个库使用了立即执行函数(IIFE)模式,将代码包裹在函数中立即运行,避免变量泄漏到全局作用域。

第二步:编写声明文件

1
2
3
4
5
6
7
8
9
10
11
12
// Calc.d.ts
// 这是声明文件,只包含类型信息,不包含任何可执行代码

// 声明 Runoob 模块,与 JS 库中的 Runoob 命名空间对应
declare module Runoob {
// 声明 Calc 类,告诉 TypeScript 这个类可以被 new 实例化
export class Calc {
// 声明 doSum 方法:接收一个 number 参数,返回一个 number
// 注意:这里只声明了方法的签名,没有方法体(大括号)
doSum(limit: number): number;
}
}

声明文件和普通 .ts 文件的最大区别:声明文件只有类型签名,没有实现代码。

第三步:在 TypeScript 代码中使用

1
2
3
4
5
6
7
8
9
// CalcTest.ts
// 三斜线指令:告诉 TypeScript 编译器引入声明文件
/// <reference path = "Calc.d.ts" />

// 创建 Calc 实例,TypeScript 现在能正确识别 obj 的类型
var obj = new Runoob.Calc();

// obj.doSum("Hello"); // 编译错误!"Hello" 是字符串,而 doSum 要求传入 number
console.log(obj.doSum(10)); // 正确调用:传入 10,期望得到 55

被注释的那一行如果取消注释,编译时会直接报错,因为声明文件中规定 doSum 只接受 number 类型参数。

第四步:编译 TypeScript

使用 tsc 命令编译:

1
tsc CalcTest.ts

编译后生成的 CalcTest.js 内容如下:

1
2
3
4
5
// CalcTest.js(tsc 命令自动生成)
/// <reference path = "Calc.d.ts" />
var obj = new Runoob.Calc();
//obj.doSum("Hello"); // 编译错误
console.log(obj.doSum(10)); // 运行结果:在控制台输出 55

三斜线指令和声明文件中的类型信息在编译产物中都不见了,只剩下纯粹的 JavaScript 运行代码。

第五步:在浏览器中运行

创建一个 HTML 页面串联所有 JS 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>菜鸟教程(runoob.com)</title>
<!-- 第 1 步:加载第三方库,让 Runoob 命名空间在全局可用 -->
<script src = "CalcThirdPartyJsLib.js"></script>
<!-- 第 2 步:加载编译后的业务代码 -->
<script src = "CalcTest.js"></script>
</head>
<body>
<h1>声明文件测试</h1>
<p>菜鸟测试一下。</p>
</body>
</html>

用浏览器打开该 HTML 文件并打开控制台(F12),可以看到输出结果 55,即 doSum(10) 的返回值:0+1+2+…+10 = 55。

声明文件测试结果

第三方类型

对于日常开发中常用的第三方库,绝大多数已经有现成的声明文件,通过 npm 安装即可:

1
2
npm install --save-dev @types/jquery
npm install --save-dev @types/lodash

只有用到非常冷门的库或内部私有库时,才需要手动编写声明文件。声明文件本质上是一份「类型说明书」,让 TypeScript 能够理解和检查纯 JavaScript 库。整个工作流程可概括为三步:

  1. 拿到一个 JS 库,分析它暴露了哪些 API。
  2. 编写 .d.ts 声明文件,描述这些 API 的类型签名。
  3. 在 TypeScript 代码中引用声明文件,即可享受完整的类型检查保护。

装饰器

装饰器是 TypeScript 的一项实验性功能。它让开发者能够在不修改原类的情况下,为类、方法、属性或参数增添额外的功能。装饰器本质上是一个函数,可在运行时被调用来修改目标对象的行为。

装饰器类型与应用位置

装饰器类型应用位置
类装饰器@ClassDecorator
属性装饰器@propertyDecorator
方法装饰器@methodDecorator
参数装饰器@paramDecorator
访问器装饰器@getterDecorator

装饰器使用 @ 符号作为语法糖,可以附加在类、方法、访问器、属性或参数上。这种模式常见于框架开发,如 Angular、TypeORM 等都大量使用装饰器来实现依赖注入、数据验证等功能。

装饰器目前是实验性功能,需要在 tsconfig.json 中显式启用。生产环境中使用时请确认项目对实验性特性的支持程度。

配置启用装饰器

使用装饰器前,需在 TypeScript 配置文件 tsconfig.json 中启用相关编译选项。

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
// 启用装饰器语法
"experimentalDecorators": true,

// 启用装饰器元数据(用于依赖注入框架)
"emitDecoratorMetadata": true
}
}
  • experimentalDecorators:启用装饰器语法支持,这是使用装饰器的前提条件。
  • emitDecoratorMetadata:在编译后的 JavaScript 中生成装饰器的元数据,供依赖注入框架使用。

类装饰器

类装饰器应用于类的构造函数,可以修改类的定义或添加额外的处理逻辑。类装饰器接收一个参数,即目标类的构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 定义一个类装饰器函数
// 参数 target 就是被装饰的类构造函数
function sealed(target: Function) {
// 打印装饰器被应用到的类名
console.log("装饰器 applied to: " + target.name);

// 使用 Object.seal 锁定构造函数和原型
// 防止在运行时添加或删除属性
Object.seal(target);
Object.seal(target.prototype);
}

// 使用 @ 语法将装饰器应用到类上
@sealed
class Person {
name: string;

constructor(name: string) {
this.name = name;
}
}

// 创建实例测试
var person = new Person("RUNOOB");
console.log("创建: " + person.name);

// 尝试添加新属性(会被阻止,因为类被 seal 了)
// person.age = 25; // 静默失败

运行结果:

1
2
装饰器 applied to: Person
创建: RUNOOB

类装饰器在类定义时就会执行,通常用于修改类行为、添加元数据或实现 AOP(面向切面编程)。

方法装饰器

方法装饰器应用于类的方法,可以修改方法的属性描述符(Property Descriptor)。方法装饰器接收三个参数:目标对象、属性名称和属性描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// 定义方法装饰器工厂
// 返回一个装饰器函数
function enumerable(value: boolean) {
// 返回装饰器函数,接收三个参数
return function (
target: any, // 所属类的原型对象
propertyKey: string, // 方法名称
descriptor: PropertyDescriptor // 属性描述符
) {
// 修改属性的 enumerable 特性
// false 表示该方法不可遍历
descriptor.enumerable = value;
};
}

class Greeter {
greeting: string;

constructor(message: string) {
this.greeting = message;
}

// 应用装饰器,设置该方法为不可枚举
@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}

var g = new Greeter("World");

// 检查 greet 方法是否可枚举
console.log("方法可枚举: " + g.propertyIsEnumerable("greet"));

// 遍历对象的属性
for (var key in g) {
console.log("属性: " + key);
}

运行结果:

1
方法可枚举: false

PropertyDescriptor 包含可枚举(enumerable)、可配置(configurable)、可写(writable)和值(value)等属性,可以根据需要修改。

访问器装饰器

访问器装饰器应用于类的 getter 和 setter 方法。与方法装饰器类似,访问器装饰器也可以修改属性描述符。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 访问器装饰器工厂
function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// 修改属性的 configurable 特性
// false 表示该访问器不可被重新配置或删除
descriptor.configurable = value;
};
}

class Point {
private _x: number = 0;
private _y: number = 0;

// 使用装饰器锁定 getter
@configurable(false)
get x() {
return this._x;
}

@configurable(false)
get y() {
return this._y;
}

set x(value: number) {
this._x = value;
}

set y(value: number) {
this._y = value;
}
}

var point = new Point();
point.x = 10;
point.y = 20;
console.log("坐标: (" + point.x + ", " + point.y + ")");

访问器装饰器不能同时应用于同一个属性的 getter 和 setter,只能选择其中一个。

属性装饰器

属性装饰器应用于类的属性定义。属性装饰器接收两个参数:目标对象和属性名称。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// 属性装饰器工厂
function format(formatString: string) {
return function (
target: any, // 类的原型对象
propertyKey: string // 属性名称
) {
// 在目标对象上存储元数据
// 使用 propertyKey + "_format" 作为键名避免冲突
Object.defineProperty(target, propertyKey + "_format", {
value: formatString,
writable: false,
enumerable: false,
configurable: true
});
};
}

class User {
// 应用属性装饰器,指定日期格式
@format("YYYY-MM-DD")
birthDate: string;

constructor(birthDate: string) {
this.birthDate = birthDate;
}
}

var user = new User("1990-01-01");
console.log("出生日期: " + user.birthDate);

// 访问存储的元数据
console.log("日期格式: " + (user as any).birthDate_format);

运行结果:

1
2
出生日期: 1990-01-01
日期格式: YYYY-MM-DD

参数装饰器

参数装饰器应用于类方法的参数,可以为参数添加元数据或标记。参数装饰器接收三个参数:目标对象、方法名称和参数在函数中的索引。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 参数装饰器
// 用于记录参数信息或进行验证
function logParameter(
target: any, // 类的原型对象
propertyKey: string, // 方法名称
parameterIndex: number // 参数在函数中的索引位置(从 0 开始)
) {
console.log("参数装饰器: " + propertyKey +
" 第 " + (parameterIndex + 1) + " 个参数");
}

class Greeter {
greeting: string;

constructor(greeting: string) {
this.greeting = greeting;
}

// 在参数前使用 @ 语法应用装饰器
greet(@logParameter name: string) {
return this.greeting + ", " + name;
}
}

var greeter = new Greeter("Hello");
greeter.greet("RUNOOB");

运行结果:

1
参数装饰器: greet 第 1 个参数

装饰器工厂

装饰器工厂是返回装饰器函数的函数。通过装饰器工厂,可以在应用装饰器时传入自定义参数,实现更灵活的配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 装饰器工厂:接收配置参数,返回装饰器函数
function color(colorCode: string) {
// colorCode 是 ANSI 转义序列的颜色代码
// 例如:34 = 蓝色,31 = 红色,32 = 绿色
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
// 保存原始方法
var originalMethod = descriptor.value;

// 重写方法,添加颜色
descriptor.value = function (...args: any[]) {
// 调用原始方法获取返回值
var result = originalMethod.apply(this, args);

// 如果在终端环境,给输出添加颜色
// ANSI 转义序列格式:\x1b[颜色码m 内容 \x1b[0m
return "\x1b[" + colorCode + "m" + result + "\x1b[0m";
};
};
}

class Logger {
// 使用装饰器工厂,传入蓝色代码 34
@color("34")
log(message: string): string {
return message;
}

@color("31")
error(message: string): string {
return message;
}

@color("32")
success(message: string): string {
return message;
}
}

var logger = new Logger();
console.log(logger.log("这是蓝色日志"));
console.log(logger.error("这是红色错误"));
console.log(logger.success("这是绿色成功"));

装饰器工厂是实际开发中最常用的形式,它允许在应用装饰器时传递参数,实现配置化。

装饰器执行顺序

当一个类上有多个装饰器时,执行顺序遵循特定的规则:

  • 装饰器从下往上应用
  • 同一类型的多个装饰器从右到左执行
  • 参数装饰器先于方法装饰器执行
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 多个装饰器叠加使用
function first() {
console.log("first 装饰器");
return function (target: any) {
console.log("first 装饰器函数");
};
}

function second() {
console.log("second 装饰器");
return function (target: any) {
console.log("second 装饰器函数");
};
}

@first()
@second()
class MyClass {
name: string;
}

var obj = new MyClass();

运行结果:

1
2
3
4
second 装饰器
first 装饰器
second 装饰器函数
first 装饰器函数

装饰器函数先执行定义(工厂求值,自下而上),然后按照从下往上的顺序执行装饰器函数。

实际应用场景

日志记录

自动记录方法调用日志。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 日志装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
var originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
console.log("调用方法: " + propertyKey + ",参数: " + JSON.stringify(args));
var result = originalMethod.apply(this, args);
console.log("方法返回: " + JSON.stringify(result));
return result;
};
}

class MathService {
@log
add(a: number, b: number): number {
return a + b;
}

@log
multiply(a: number, b: number): number {
return a * b;
}
}

var math = new MathService();
console.log("计算结果: " + math.add(5, 3));

运行结果:

1
2
3
调用方法: add,参数: [5,3]
方法返回: 8
计算结果: 8

权限验证

实现方法级别的权限检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 模拟当前用户角色
var currentUser = { role: "admin" };

// 权限装饰器
function requireRole(role: string) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
var originalMethod = descriptor.value;

descriptor.value = function (...args: any[]) {
if (currentUser.role !== role) {
console.log("权限不足,无法执行 " + propertyKey);
return null;
}
return originalMethod.apply(this, args);
};
};
}

class AdminService {
@requireRole("admin")
deleteUser(id: number): string {
return "删除用户 " + id + " 成功";
}

@requireRole("admin")
viewUser(id: number): string {
return "查看用户 " + id;
}
}

var admin = new AdminService();
console.log(admin.viewUser(1));
console.log(admin.deleteUser(1));

// 模拟普通用户
currentUser = { role: "user" };
console.log(admin.deleteUser(2));

运行结果:

1
2
3
查看用户 1
删除用户 1 成功
权限不足,无法执行 deleteUser

在项目中使用装饰器时,建议创建专门的装饰器工具类或函数库,统一管理装饰器的定义和使用。

路径映射

paths 是 tsconfig.json 中的一个选项,用于配置模块路径别名。它可以让导入路径更简洁,同时保持代码结构清晰。

为什么需要路径映射

随着项目增长,目录结构加深,相对路径如 ../../../components/Button 变得难以阅读和维护。路径映射让我们可以使用别名如 @/components/Button 来代替冗长的相对路径。

路径别名让导入路径更简洁,同时便于调整项目结构。

基本配置

在 tsconfig.json 中配置 pathsbaseUrl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"compilerOptions": {
// 基准路径:所有路径别名的根目录
"baseUrl": ".",
// 路径映射配置
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@utils/*": ["src/utils/*"],
"@services/*": ["src/services/*"],
"@assets/*": ["src/assets/*"],
"@types/*": ["src/types/*"]
}
}
}

设置 baseUrl 后,paths 中的路径将相对于此目录解析。

使用路径别名

配置完成后,可以在代码中使用别名导入模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用路径别名导入
import { Button } from '@/components/Button';
import { User } from '@types/user';
import { fetchUser } from '@services/userApi';
import { formatDate } from '@utils/date';

// 导入样式
import styles from '@/components/Button.module.css';

// 导入图片
import logo from '@assets/logo.png';

// 使用导入的模块
const handleClick = () => {
console.log('按钮点击');
};

const userButton = new Button({
text: '用户',
onClick: handleClick
});

console.log('组件加载成功');

在 paths 配置中使用 * 作为通配符,导入时用 * 匹配实际路径。

Webpack 别名配置

如果使用 Webpack,需要配置 resolve.alias 以获得运行时支持。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// webpack.config.js
const path = require('path');

module.exports = {
resolve: {
// 路径别名配置,需要与 tsconfig.json 保持一致
alias: {
'@': path.resolve(__dirname, 'src'),
'@components': path.resolve(__dirname, 'src/components'),
'@utils': path.resolve(__dirname, 'src/utils'),
'@services': path.resolve(__dirname, 'src/services'),
'@types': path.resolve(__dirname, 'src/types'),
'@assets': path.resolve(__dirname, 'src/assets')
},
// 文件扩展名
extensions: ['.ts', '.tsx', '.js', '.jsx', '.json']
}
};

Webpack 的 alias 配置必须与 TypeScript 的 paths 配置保持一致,否则运行时会出现模块找不到的错误。

Vite 配置

使用 Vite 时,需要同时配置 tsconfig.json 和 vite.config.ts。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';
import path from 'path';

export default defineConfig({
plugins: [react()],
resolve: {
alias: {
'@': path.resolve(__dirname, './src'),
'@components': path.resolve(__dirname, './src/components'),
'@utils': path.resolve(__dirname, './src/utils'),
'@services': path.resolve(__dirname, './src/services'),
'@types': path.resolve(__dirname, './src/types'),
'@assets': path.resolve(__dirname, './src/assets')
}
}
});

Vite 使用 Rollup 作为打包引擎,需要在 resolve.alias 中配置别名。

多项目路径配置

在 Monorepo 项目中,可以配置跨包的路径别名。

1
2
3
4
5
6
7
8
9
10
11
12
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@my-ui/button": ["packages/ui-button/src/index.ts"],
"@my-ui/modal": ["packages/ui-modal/src/index.ts"],
"@my-utils/date": ["packages/utils-date/src/index.ts"],
"@my-hooks/useFetch": ["packages/hooks-use-fetch/src/index.ts"]
}
}
}

在 Monorepo 项目中,paths 可以引用其他包的源码路径。

注意事项

  • baseUrl 必需:paths 需要与 baseUrl 配合使用
  • 通配符匹配:使用 * 匹配任意路径
  • 保持一致:Webpack/Vite 配置必须与 tsconfig 保持一致
  • 相对路径:paths 中的路径是相对于 baseUrl 的

使用路径别名可以让代码更简洁,但不要滥用,建议统一命名规范。

项目引用

项目引用是 TypeScript 提供的组织大型项目的功能,允许将 TypeScript 项目拆分为更小的部分。它可以实现增量构建、更好的代码组织和更快的编译速度。

为什么需要项目引用

随着项目增长,单一的 tsconfig.json 会导致编译速度变慢。项目引用允许将项目拆分为独立的子项目,每个子项目可以独立编译,这不仅提高了编译速度,还提供了更好的代码组织方式。

项目引用允许一个 TypeScript 项目引用其他项目,实现增量编译和更好的代码组织。

创建引用项目

首先创建被引用的子项目。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// packages/utils/tsconfig.json
{
// 继承基础配置
"extends": "../../tsconfig.base.json",

"compilerOptions": {
"outDir": "./dist",
"declarationDir": "./dist/types",
"declaration": true,
"sourceMap": true,
// 是否为库
"composite": true
},

"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

composite 设置为 true 启用项目引用功能,这是被引用的项目必须设置的选项。

主项目配置

在主项目的 tsconfig.json 中配置 references。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// tsconfig.json (主项目)
{
"extends": "./tsconfig.base.json",

"compilerOptions": {
"outDir": "./dist",
"declarationDir": "./dist/types",
"declaration": true,
"sourceMap": true
},

// 项目引用配置
"references": [
{ "path": "./packages/utils" },
{ "path": "./packages/ui" },
{ "path": "./packages/types" }
],

"include": ["src/**/*"]
}

references 数组中的每个对象指定一个引用的项目路径,path 相对于当前项目。

类型引用

在代码中使用被引用项目的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// src/index.ts

// 导入工具函数(来自 utils 包)
import { formatDate, formatCurrency } from '@my-utils/format';

// 导入组件(来自 ui 包)
import { Button, Modal, Input } from '@my-ui/core';

// 导入类型(来自 types 包)
import { User, ApiResponse } from '@my-types/common';

// 定义用户数据
const user: User = {
id: 1,
name: "Alice",
email: "alice@example.com"
};

// 格式化日期
const dateStr = formatDate(new Date(), "YYYY-MM-DD");
console.log("日期: " + dateStr);

// 格式化货币
const price = formatCurrency(999);
console.log("价格: " + price);

// 创建用户界面
const button = new Button({
text: "提交",
variant: "primary"
});

console.log("应用初始化成功");

被引用项目的导出可以直接导入,TypeScript 会自动解析类型。

增量构建

项目引用支持增量构建,只编译修改的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 构建整个项目(包括所有引用)
npm run build

# 只构建主项目(不重新构建依赖)
npm run build -- --build

# 清理并重建
npm run clean
npm run build

# 增量构建(推荐)
# 修改某个包后,只重新编译该包和依赖它的包
npx tsc -b packages/utils
npx tsc -b packages/ui
npx tsc -b .

使用 -b (build) 选项时,TypeScript 会自动检测哪些项目需要重新编译。

注意事项

  • composite 选项:被引用的项目必须设置 composite: true
  • 输出目录:每个项目需要独立的输出目录
  • 声明文件:被引用项目需要生成声明文件
  • 构建顺序:依赖的项目需要先构建

将公共代码拆分为独立包,使用项目引用进行管理,提高编译效率。

Monorepo 配置

Monorepo 是一种将多个项目放在同一个代码仓库中的开发模式。TypeScript 通过项目引用和工具链支持 Monorepo,可以高效管理多个包。

为什么需要 Monorepo

当项目包含多个包(如工具库、组件库、应用程序)时,传统方式需要维护多个代码仓库。Monorepo 将包集中管理,共享代码更方便,版本管理更统一。

Monorepo(单一仓库)将多个相关项目放在同一个代码仓库中,便于代码共享和协调开发。

项目结构

根目录包含 package.jsontsconfig.jsonlerna.jsonturbo.json,以及 packages/ 目录下的子包。管理工具包括 npm workspaces、yarn workspaces、pnpm、lerna、turbo。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
my-monorepo/
├── packages/
│ ├── utils/
│ │ ├── src/
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ ├── ui-components/
│ │ ├── src/
│ │ │ ├── Button.tsx
│ │ │ └── index.ts
│ │ ├── package.json
│ │ └── tsconfig.json
│ └── app/
│ ├── src/
│ │ └── index.tsx
│ ├── package.json
│ └── tsconfig.json
├── package.json
├── tsconfig.base.json
└── pnpm-workspace.yaml

所有包都放在 packages 目录下,每个包有独立的 package.json 和 tsconfig.json。

pnpm Workspace

pnpm 原生支持 Workspace 功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
// 根目录 package.json
{
"private": true,
"packages": ["packages/*"],
"devDependencies": {
"typescript": "^5.0.0"
},
"scripts": {
"build": "pnpm -r run build",
"clean": "pnpm -r run clean",
"type-check": "pnpm -r run type-check"
}
}

pnpm 的 Workspace 功能可以自动将 packages 目录下的包链接在一起。

基础 TypeScript 配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// tsconfig.base.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
}
}

各包的 tsconfig.json 继承基础配置,只覆盖需要自定义的选项。

工具包配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// packages/utils/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"declarationDir": "./dist/types",
"declaration": true,
"declarationMap": true,
"module": "ESNext",
"composite": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

启用项目引用后,TypeScript 可以增量编译此包。

应用程序配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// packages/app/tsconfig.json
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"jsx": "react-jsx",
"paths": {
"@my-utils/*": ["../utils/src/*"],
"@my-ui/*": ["../ui-components/src/*"]
}
},
"references": [
{ "path": "../utils" },
{ "path": "../ui-components" }
]
}

可以配置路径别名直接引用同仓库的其他包。

包之间的依赖

1
2
3
4
5
6
7
8
9
// packages/app/package.json
{
"name": "@my-org/app",
"dependencies": {
"@my-org/utils": "workspace:*",
"@my-org/ui-components": "workspace:*",
"react": "^18.0.0"
}
}

使用 workspace:* 指向同仓库的其他包,pnpm 会自动解析。

构建脚本

1
2
3
4
5
6
7
8
9
10
11
// 根目录 package.json scripts
{
"scripts": {
"build": "pnpm -r run build",
"clean": "pnpm -r run clean",
"type-check": "pnpm -r run type-check",
"test": "pnpm -r run test",
"build:watch": "pnpm -r --parallel run build:watch",
"dev": "pnpm --filter @my-org/app run dev"
}
}

递归执行所有包的同名脚本。

注意事项

  • 使用 @org-name/package 格式命名包
  • 每个包可独立版本管理
  • workspace:* 引用同仓库包
  • 被依赖的包需要先构建

Monorepo 适合管理多个相关项目,可以显著提高代码复用和开发效率。当项目包含多个相关包时,优先考虑 Monorepo 方案。

从 JS 迁移

将现有 JavaScript 项目逐步迁移到 TypeScript,核心策略是渐进式迁移。

迁移策略

  1. 添加 tsconfig.json
  2. 重命名 .js.ts
  3. 逐步添加类型注解
  4. 启用严格模式

配置 tsconfig.json

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"compilerOptions": {
// 初始阶段:宽松配置
"target": "ES2020",
"module": "commonjs",
"strict": false,
"noImplicitAny": false,
"strictNullChecks": false,
"skipLibCheck": true,

// 允许 JS 文件
"allowJs": true,
"checkJs": false,

// 输出目录
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

逐步启用严格检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 阶段 1: 基础迁移
{
"compilerOptions": {
"strict": false,
"noImplicitAny": false
}
}

// 阶段 2: 启用类型检查
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}

// 阶段 3: 完全严格
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictPropertyInitialization": true
}
}

JSDoc 类型注释

在 JavaScript 中使用 JSDoc 添加类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// utils.js
/**
* @param {number} a
* @param {number} b
* @returns {number}
*/
function add(a, b) {
return a + b;
}

/**
* @typedef {Object} User
* @property {number} id
* @property {string} name
* @property {string} email
*/

/**
* @param {number} id
* @returns {Promise<User>}
*/
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json());
}

类型声明文件

为没有类型定义的模块创建声明。

1
2
3
4
5
6
7
8
// src/types/my-module.d.ts
declare module "my-module" {
export function doSomething(param: string): void;
export class MyClass {
constructor(options: { name: string });
name: string;
}
}

declare 关键字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 声明全局变量
declare var GLOBAL_CONFIG: {
apiUrl: string;
version: string;
};

// 声明全局函数
declare function myFunction(param: string): void;

// 声明命名空间
declare namespace MyNamespace {
function doSomething(): void;
}

// 使用
console.log(GLOBAL_CONFIG.apiUrl);
myFunction("hello");
MyNamespace.doSomething();

迁移工具

  • tsc --allowJs:编译 JS 文件
  • checkJs:检查 JS 类型
  • // @ts-check:单文件类型检查
  • // @ts-ignore:忽略错误
1
2
3
4
// legacy.js
// @ts-check
// @ts-ignore
var result = someLegacyFunction();

最佳实践

  1. 从关键模块开始迁移
  2. 添加单元测试
  3. 逐步启用严格模式
  4. 使用 JSDoc 注释
  5. 创建类型声明文件

迁移的核心是渐进式:逐步迁移、JSDoc 类型注释、声明文件 .d.ts、严格模式分阶段启用。

单元测试

TypeScript 项目中的单元测试实践,确保代码质量。单元测试可以验证代码的正确性,TypeScript 的类型系统与测试框架完美结合,可以编写类型安全的测试代码。

单元测试原则

每个测试只验证一件事;遵循 Arrange-Act-Assert 结构;测试应该相互独立。

单元测试可以快速发现回归问题,确保代码改动不会破坏现有功能。

测试框架配置

Jest 是 TypeScript 项目最流行的测试框架。

安装 Jest

1
2
3
4
5
6
7
# 安装 Jest 和相关依赖
# - ts-jest: 让 Jest 能够运行 TypeScript
# - @types/jest: Jest 的类型定义
npm install --save-dev jest ts-jest @types/jest

# 初始化 Jest 配置
npx ts-jest config:init

ts-jest 是一个预处理器,让 Jest 能够直接运行 TypeScript 文件,无需手动编译。

配置 jest.config.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
module.exports = {
// 使用 ts-jest 预设
preset: 'ts-jest',
// 测试环境:node 或 browser
testEnvironment: 'node',
// 测试文件目录
roots: ['<rootDir>/src'],
// 测试文件匹配模式
testMatch: ['**/__tests__/**/*.ts'],
// 支持的文件扩展名
moduleFileExtensions: ['ts', 'js', 'json'],
// 收集覆盖率的文件
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts' // 排除类型声明文件
]
};

测试文件通常放在 __tests__ 目录或以 .test.ts 结尾。

测试函数

首先编写需要测试的业务代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// src/utils/calculator.ts
export class Calculator {
add(a: number, b: number): number {
return a + b;
}

subtract(a: number, b: number): number {
return a - b;
}

multiply(a: number, b: number): number {
return a * b;
}

divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}
}

然后编写对应的测试代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// src/utils/calculator.test.ts
import { Calculator } from "./calculator";

describe("Calculator", () => {
let calculator: Calculator;

// 每个测试前创建新的 Calculator 实例
beforeEach(() => {
calculator = new Calculator();
});

describe("add", () => {
it("should add two numbers", () => {
expect(calculator.add(2, 3)).toBe(5);
});

it("should handle negative numbers", () => {
expect(calculator.add(-1, 1)).toBe(0);
});
});

describe("divide", () => {
it("should divide two numbers", () => {
expect(calculator.divide(10, 2)).toBe(5);
});

it("should throw error when dividing by zero", () => {
expect(() => calculator.divide(10, 0)).toThrow();
});
});
});

运行结果:

1
2
3
4
5
6
7
Calculator
add
should add two numbers
should handle negative numbers
divide
should divide two numbers
should throw error when dividing by zero

describe 用于分组测试,it(或 test)用于定义单个测试用例。

测试 Service

测试 Service 层的业务逻辑。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// src/services/userService.ts
export interface User {
id: number;
name: string;
}

export class UserService {
private users: User[] = [];
private nextId = 1;

createUser(name: string): User {
const user = { id: this.nextId++, name };
this.users.push(user);
return user;
}

getUser(id: number): User | undefined {
return this.users.find(u => u.id === id);
}

getAllUsers(): User[] {
return [...this.users];
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// src/services/userService.test.ts
import { UserService } from "./userService";

describe("UserService", () => {
let service: UserService;

beforeEach(() => {
service = new UserService();
});

describe("createUser", () => {
it("should create a user with id", () => {
const user = service.createUser("Alice");
expect(user.id).toBe(1);
expect(user.name).toBe("Alice");
});

it("should increment id for each user", () => {
const user1 = service.createUser("Alice");
const user2 = service.createUser("Bob");
expect(user2.id).toBe(user1.id + 1);
});
});

describe("getUser", () => {
it("should return user by id", () => {
const created = service.createUser("Alice");
const found = service.getUser(created.id);
expect(found?.name).toBe("Alice");
});

it("should return undefined for non-existent id", () => {
const found = service.getUser(999);
expect(found).toBeUndefined();
});
});
});

运行结果:

1
2
3
4
5
6
7
UserService
createUser
✓ should create a user with id
✓ should increment id for each user
getUser
✓ should return user by id
✓ should return undefined for non-existent id

每个测试用例应该独立,使用 beforeEach 确保每个测试都有干净的状态。

Mock

使用 Mock 模拟依赖,如外部 API、数据库等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Mock 函数:创建模拟函数
const mockCallback = jest.fn(x => x * 2);

// 使用模拟函数
[1, 2, 3].forEach(mockCallback);

// 验证函数被调用了 3 次
expect(mockCallback).toHaveBeenCalledTimes(3);
// 验证函数被调用时的参数
expect(mockCallback).toHaveBeenCalledWith(2);

// Mock 模块:模拟整个模块
jest.mock("./api", () => ({
fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: "Alice" }))
}));

当测试的代码依赖外部系统时,使用 Mock 可以隔离依赖,只测试目标代码的逻辑。

注意事项

  • 测试文件位置:放在 __tests__ 目录或使用 .test.ts 后缀
  • 测试命名:使用描述性的测试名称,说明预期行为
  • 独立测试:每个测试应该独立运行,不依赖其他测试
  • 覆盖率:关注核心业务逻辑的测试覆盖率

测试应该快速、可靠、相互独立。遵循 AAA 原则:Arrange(准备)、Act(执行)、Assert(断言)。

设计模式

设计模式是软件开发中经过验证的解决方案,可以帮助我们编写可维护、可扩展的代码。TypeScript 的类型系统让许多经典设计模式得以用类型安全的方式实现。

设计模式分类

  • 创建型模式:Factory、Singleton、Builder
  • 结构型模式:Decorator、Adapter、Proxy
  • 行为型模式:Observer、Strategy、Command
  • TypeScript 特有模式:类型安全的依赖注入、泛型工厂、条件类型选择

设计模式是软件设计中常见问题的可重用解决方案,是代码设计经验的总结。

单例模式(Singleton)

确保一个类只有一个实例,并提供全局访问点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class Singleton {
// 存储单例实例
private static instance: Singleton;
private static _data: string = "";

// 私有构造函数,防止外部实例化
private constructor() {}

// 获取单例实例的静态方法
public static getInstance(): Singleton {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}
return Singleton.instance;
}

public setData(data: string): void {
Singleton._data = data;
}

public getData(): string {
return Singleton._data;
}
}

// 测试单例模式
const instance1 = Singleton.getInstance();
const instance2 = Singleton.getInstance();

console.log("是同一实例: " + (instance1 === instance2));

instance1.setData("Hello Singleton");
console.log("数据: " + instance2.getData());

运行结果:

1
2
是同一实例: true
数据: Hello Singleton

通过将构造函数设为 private,防止外部使用 new 创建实例。

工厂模式(Factory)

使用泛型工厂创建类型安全的对象实例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
// 定义产品接口
interface Product {
name: string;
price: number;
getDescription(): string;
}

// 具体产品:电子产品
class ElectronicProduct implements Product {
constructor(
public name: string,
public price: number,
public warranty: number
) {}

getDescription(): string {
return `${this.name} - ¥${this.price} (保修${this.warranty}年)`;
}
}

// 具体产品:服装
class ClothingProduct implements Product {
constructor(
public name: string,
public price: number,
public size: string
) {}

getDescription(): string {
return `${this.name} - ¥${this.price} (尺码: ${this.size})`;
}
}

// 工厂类
class ProductFactory {
// 泛型工厂方法
static create<T extends Product>(
type: new (...args: any[]) => T,
...args: any[]
): T {
return new type(...args);
}
}

// 使用工厂创建产品
const laptop = ProductFactory.create(ElectronicProduct, "笔记本电脑", 5999, 2);
const shirt = ProductFactory.create(ClothingProduct, "T恤", 199, "L");

console.log(laptop.getDescription());
console.log(shirt.getDescription());

使用泛型约束确保返回具体的产品类型。

装饰器模式(Decorator)

使用装饰器为对象动态添加功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 基础咖啡接口
interface Coffee {
getCost(): number;
getDescription(): string;
}

// 基础咖啡实现
class SimpleCoffee implements Coffee {
getCost(): number {
return 10;
}

getDescription(): string {
return "咖啡";
}
}

// 装饰器基类
abstract class CoffeeDecorator implements Coffee {
constructor(protected coffee: Coffee) {}

getCost(): number {
return this.coffee.getCost();
}

getDescription(): string {
return this.coffee.getDescription();
}
}

// 牛奶装饰器
class MilkDecorator extends CoffeeDecorator {
getCost(): number {
return this.coffee.getCost() + 2;
}

getDescription(): string {
return this.coffee.getDescription() + ", 牛奶";
}
}

// 糖装饰器
class SugarDecorator extends CoffeeDecorator {
getCost(): number {
return this.coffee.getCost() + 1;
}

getDescription(): string {
return this.coffee.getDescription() + ", 糖";
}
}

// 使用装饰器
let coffee: Coffee = new SimpleCoffee();
console.log(coffee.getDescription() + " - ¥" + coffee.getCost());

coffee = new MilkDecorator(coffee);
console.log(coffee.getDescription() + " - ¥" + coffee.getCost());

coffee = new SugarDecorator(coffee);
console.log(coffee.getDescription() + " - ¥" + coffee.getCost());

可以在不修改原类的情况下,动态添加新功能。

观察者模式(Observer)

定义对象间的一对多依赖关系,当对象状态改变时,所有依赖者都会收到通知。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// 观察者接口
interface Observer {
update(message: string): void;
}

// 主题接口
interface Subject {
attach(observer: Observer): void;
detach(observer: Observer): void;
notify(): void;
}

// 具体主题:消息中心
class MessageCenter implements Subject {
private observers: Observer[] = [];
private message: string = "";

attach(observer: Observer): void {
this.observers.push(observer);
}

detach(observer: Observer): void {
const index = this.observers.indexOf(observer);
if (index > -1) {
this.observers.splice(index, 1);
}
}

notify(): void {
for (const observer of this.observers) {
observer.update(this.message);
}
}

publish(message: string): void {
this.message = message;
console.log("发布消息: " + message);
this.notify();
}
}

// 具体观察者:用户
class UserObserver implements Observer {
constructor(public name: string) {}

update(message: string): void {
console.log(`[${this.name}] 收到消息: ${message}`);
}
}

// 使用观察者模式
const center = new MessageCenter();

const user1 = new UserObserver("用户A");
const user2 = new UserObserver("用户B");

center.attach(user1);
center.attach(user2);

center.publish("新功能上线了!");

观察者模式实现了主题和观察者之间的松耦合。

策略模式(Strategy)

定义一系列算法,把它们一个个封装起来,使它们可以相互替换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 支付策略接口
interface PaymentStrategy {
pay(amount: number): void;
}

class WechatPayStrategy implements PaymentStrategy {
pay(amount: number): void {
console.log(`使用微信支付 ¥${amount}`);
}
}

class AlipayStrategy implements PaymentStrategy {
pay(amount: number): void {
console.log(`使用支付宝支付 ¥${amount}`);
}
}

class CardPayStrategy implements PaymentStrategy {
pay(amount: number): void {
console.log(`使用银行卡支付 ¥${amount}`);
}
}

// 支付上下文
class PaymentContext {
private strategy: PaymentStrategy;

constructor(strategy: PaymentStrategy) {
this.strategy = strategy;
}

setStrategy(strategy: PaymentStrategy): void {
this.strategy = strategy;
}

pay(amount: number): void {
this.strategy.pay(amount);
}
}

// 使用策略模式
const context = new PaymentContext(new WechatPayStrategy());
context.pay(100);

context.setStrategy(new AlipayStrategy());
context.pay(200);

context.setStrategy(new CardPayStrategy());
context.pay(300);

策略模式可以在运行时切换算法,提供了很大的灵活性。

依赖注入(Dependency Injection)

通过构造函数注入依赖,是 TypeScript 中最常用的模式之一。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// 定义服务接口
interface Logger {
log(message: string): void;
}

interface Storage {
save(key: string, data: any): void;
}

// 具体服务实现
class ConsoleLogger implements Logger {
log(message: string): void {
console.log("[日志]: " + message);
}
}

class LocalStorage implements Storage {
save(key: string, data: any): void {
console.log(`保存 ${key}: ${JSON.stringify(data)}`);
}
}

// 使用依赖注入的服务
class UserService {
constructor(
private logger: Logger,
private storage: Storage
) {}

createUser(name: string): void {
const user = { name, createdAt: new Date() };
this.logger.log("创建用户: " + name);
this.storage.save("user", user);
}
}

// 注入依赖
const logger = new ConsoleLogger();
const storage = new LocalStorage();
const userService = new UserService(logger, storage);

userService.createUser("Alice");

依赖注入实现了高层模块不依赖低层模块,而是依赖抽象接口。

建造者模式(Builder)

将一个复杂对象的构建与它的表示分离,使得同样的构建过程可以创建不同的表示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 建造者接口
interface Builder<T> {
build(): T;
}

// 复杂对象:用户配置
interface UserConfig {
name: string;
email: string;
age?: number;
role?: string;
theme?: string;
}

// 用户配置建造者
class UserConfigBuilder implements Builder<UserConfig> {
private config: Partial<UserConfig> = {};

setName(name: string): this {
this.config.name = name;
return this;
}

setEmail(email: string): this {
this.config.email = email;
return this;
}

setAge(age: number): this {
this.config.age = age;
return this;
}

setRole(role: string): this {
this.config.role = role;
return this;
}

setTheme(theme: string): this {
this.config.theme = theme;
return this;
}

build(): UserConfig {
if (!this.config.name || !this.config.email) {
throw new Error("Name and email are required");
}
return this.config as UserConfig;
}
}

// 使用建造者
const builder = new UserConfigBuilder();
const config = builder
.setName("Alice")
.setEmail("alice@example.com")
.setAge(25)
.setRole("admin")
.setTheme("dark")
.build();

console.log("用户配置:", JSON.stringify(config, null, 2));

建造者模式支持链式调用,使代码更简洁易读。

注意事项

  • 类型安全:利用 TypeScript 的类型系统确保模式实现的安全
  • 不要过度:只在真正需要时才使用设计模式
  • 保持简单:优先使用简单的解决方案
  • 团队共识:在团队内部统一设计模式的使用

设计模式是工具,不是教条。选择适合项目实际情况的模式。

性能优化

TypeScript 项目的性能优化涉及编译速度、运行时性能和代码体积等多个方面。TypeScript 虽然提供了强大的类型系统,但使用不当会影响编译速度和运行性能。大型项目可能需要数分钟编译,严重影响开发体验。

性能优化的目标是:更快的编译速度、更小的打包体积、更高的运行时性能。

编译配置优化

通过 tsconfig.json 配置优化编译速度。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"compilerOptions": {
// 启用增量编译,保存上次编译信息
"incremental": true,

// 跳过 node_modules 的类型检查
// 大幅提升编译速度
"skipLibCheck": true,

// 跳过声明文件的生成(仅在最终构建时生成)
"noEmit": true,

// 启用快速增量构建
"assumeChangesOnlyAffectDirectDependencies": true,

// 启用缓存
"tsBuildInfoFile": ".tsbuildinfo",

// 不需要解析的文件
"exclude": [
"node_modules",
"dist",
"build",
"**/*.test.ts"
]
}
}

skipLibCheck 是最重要的优化选项,可以将编译时间减少 50% 以上。

项目引用优化

使用项目引用将大型项目拆分为较小的模块,实现增量编译。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// packages/utils/tsconfig.json
{
"extends": "../../tsconfig.base.json",

"compilerOptions": {
"composite": true,
"outDir": "./dist",
"declaration": true,
"declarationMap": true
},

"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}

composite 启用后,TypeScript 会生成 .tsbuildinfo 文件来加速后续编译。

类型推断优化

利用 TypeScript 的类型推断,避免过度标注。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// 不好的写法:过度标注类型
const name: string = "Alice";
const age: number = 25;
const isActive: boolean = true;

// 好的写法:利用类型推断
const name = "Alice";
const age = 25;
const isActive = true;

// 函数返回值类型可以省略(TypeScript 会自动推断)
function add(a: number, b: number) {
return a + b;
}

// 复杂对象使用类型推断
const user = {
id: 1,
name: "Bob",
email: "bob@example.com"
};
// TypeScript 会推断出:
// { id: number; name: string; email: string }

// 只有在类型推断不准确时才需要显式标注
const elements: HTMLElement[] = [];

TypeScript 的类型推断已经很智能,大部分情况下不需要显式标注类型。

避免使用 any

使用 any 会消除类型检查的好处并影响运行时性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 不好的写法:使用 any
function processData(data: any): any {
return data.value;
}

// 好的写法:使用 unknown 或具体类型
function processData<T extends { value: string }>(data: T): string {
return data.value;
}

// 如果真的不知道类型,使用 unknown
function parseJSON(json: string): unknown {
return JSON.parse(json);
}

// 使用时进行类型检查
const data = parseJSON('{"key": "value"}');
if (typeof data === "object" && data !== null) {
const obj = data as { key: string };
console.log(obj.key);
}

// 更好的做法:使用泛型
function identity<T>(value: T): T {
return value;
}

const result = identity("hello");
console.log("结果: " + result);

unknown 是类型安全的 any,使用前需要进行类型检查。

接口 vs 类型别名

根据场景选择合适的类型定义方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 接口:适合定义对象类型,支持声明合并
interface User {
id: number;
name: string;
}

// 扩展接口
interface User {
email: string;
}

// 类型别名:适合联合类型、元组、函数类型
type ID = string | number;
type Status = "pending" | "success" | "error";
type Callback = (data: string) => void;

// 工具类型通常使用 type
type PartialUser = Partial<User>;
type ReadonlyUser = Readonly<User>;

// 性能考虑:接口的编译速度通常比类型别名快
// 对于简单的对象类型,可以使用 interface
interface Point {
x: number;
y: number;
}

// 对于联合类型,使用 type
type Shape = Circle | Square | Triangle;

对象类型使用接口,联合类型使用 type,功能类型使用 type。

构建工具优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// vite.config.ts
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
plugins: [react()],

build: {
// 启用代码拆分
rollupOptions: {
output: {
// 手动分包
manualChunks: {
'vendor': ['react', 'react-dom'],
'utils': ['lodash', 'axios']
}
}
},

// 压缩代码
minify: 'terser',

// 生成 sourcemap
sourcemap: false,

// 块大小限制
chunkSizeWarningLimit: 500
},

// 开发服务器优化
server: {
hmr: {
overlay: true
}
},

// 优化依赖解析
optimizeDeps: {
include: ['react', 'react-dom'],
exclude: ['some-large-library']
}
});

使用 manualChunks 将大型库分离,减少主包体积。

Tree Shaking

配置模块以支持 Tree Shaking,消除未使用的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// src/utils/index.ts
// 使用 ES 模块导出,支持 Tree Shaking
export function add(a: number, b: number): number {
return a + b;
}

export function subtract(a: number, b: number): number {
return a - b;
}

export function multiply(a: number, b: number): number {
return a * b;
}

export function divide(a: number, b: number): number {
if (b === 0) {
throw new Error("Cannot divide by zero");
}
return a / b;
}

// 具名导出比默认导出更有利于 Tree Shaking
// 错误:export default 会阻止 Tree Shaking
// export default { add, subtract, multiply, divide };

// 正确:使用具名导出
console.log("工具模块加载");

使用具名导出而不是默认导出,可以让构建工具更好地进行 Tree Shaking。

注意事项

  • skipLibCheck:生产环境必须开启
  • 增量编译:开发环境建议开启
  • 避免 any:使用 unknown 代替
  • Tree Shaking:使用 ES 模块和具名导出

在开发初期就设置好优化配置,避免后期重构。定期检查编译时间和包体积,持续优化项目性能。

综合项目:类型设计要点

通过一个任务管理系统的类型设计,综合运用 TypeScript 的各种特性。这里聚焦于纯 TypeScript 类型层面的设计,不展开具体框架用法。

类型分层

将输入类型、输出类型、过滤类型分开定义,便于维护。先定义类型,再编写实现代码。

任务类型定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// src/types/task.ts

// 任务状态枚举
export type TaskStatus = "pending" | "in-progress" | "completed";

// 任务优先级枚举
export type TaskPriority = "low" | "medium" | "high";

// 任务接口定义
export interface Task {
id: string; // 任务ID
title: string; // 任务标题
description?: string; // 任务描述(可选)
status: TaskStatus; // 任务状态
priority: TaskPriority; // 任务优先级
createdAt: string; // 创建时间
updatedAt: string; // 更新时间
dueDate?: string; // 截止日期(可选)
tags?: string[]; // 标签(可选)
}

// 创建任务的输入类型
export interface CreateTaskInput {
title: string;
description?: string;
priority: TaskPriority;
dueDate?: string;
tags?: string[];
}

// 更新任务的输入类型
export interface UpdateTaskInput {
title?: string;
description?: string;
status?: TaskStatus;
priority?: TaskPriority;
dueDate?: string;
tags?: string[];
}

// 任务过滤选项
export interface TaskFilter {
status?: TaskStatus;
priority?: TaskPriority;
search?: string;
}

将输入类型、输出类型、过滤类型分开定义,便于维护。CreateTaskInput 不包含 idstatuscreatedAt 等由系统生成的字段,而 UpdateTaskInput 的所有字段都是可选的。

API 类型定义

定义统一的 API 响应格式和错误处理类型,便于前后端对接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
// src/types/api.ts

// 通用 API 响应类型
export interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
message?: string;
}

// 分页元数据
export interface PaginationMeta {
total: number;
page: number;
pageSize: number;
totalPages: number;
}

// 分页响应类型
export interface PaginatedResponse<T> {
items: T[];
meta: PaginationMeta;
}

// 请求错误类型
export interface ApiError {
code: string;
message: string;
details?: Record<string, string>;
}

// HTTP 方法类型
export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";

// 任务相关的 API 端点
export interface TaskEndpoints {
getAll: "/api/tasks";
getById: "/api/tasks/:id";
create: "/api/tasks";
update: "/api/tasks/:id";
delete: "/api/tasks/:id";
}

ApiResponse<T> 使用泛型封装统一的响应结构,PaginatedResponse<T> 复用同一个泛型支持分页场景,HttpMethod 用字面量联合类型约束合法的 HTTP 方法。

服务层类型约束

业务逻辑集中在服务层,类型签名清晰地表达了输入输出契约。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// src/services/taskService.ts
import {
Task,
CreateTaskInput,
UpdateTaskInput,
TaskFilter,
TaskStatus
} from "../types/task";

class TaskService {
// 获取所有任务,支持可选过滤
getAll(filter?: TaskFilter): Task[] {
// 实现省略
return [];
}

// 根据ID获取任务,可能不存在
getById(id: string): Task | undefined {
// 实现省略
return undefined;
}

// 创建任务,返回完整任务
create(input: CreateTaskInput): Task {
// 实现省略
return {} as Task;
}

// 更新任务,不存在时返回 null
update(id: string, input: UpdateTaskInput): Task | null {
// 实现省略
return null;
}

// 删除任务,返回是否成功
delete(id: string): boolean {
// 实现省略
return false;
}

// 更新任务状态,复用 update 方法
updateStatus(id: string, status: TaskStatus): Task | null {
return this.update(id, { status });
}
}

export const taskService = new TaskService();

服务层的方法签名就是最好的契约文档:getById 返回 Task | undefined 明确表达了可能不存在;update 返回 Task | null 区分了"更新成功"与"任务不存在"两种情况;updateStatus 复用 update 体现了类型组合的便利。

自定义 Hook 的类型设计

将状态管理和业务逻辑封装时,显式定义返回值类型可以让消费方获得完整的类型提示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// src/hooks/useTasks.ts
import {
Task,
CreateTaskInput,
UpdateTaskInput,
TaskFilter,
TaskStatus
} from "../types/task";

// Hook 返回的状态类型
interface UseTasksReturn {
tasks: Task[];
loading: boolean;
error: string | null;
filter: TaskFilter;
// 操作方法
createTask: (input: CreateTaskInput) => Promise<void>;
updateTask: (id: string, input: UpdateTaskInput) => Promise<void>;
deleteTask: (id: string) => Promise<void>;
updateStatus: (id: string, status: TaskStatus) => Promise<void>;
setFilter: (filter: TaskFilter) => void;
refresh: () => void;
}

显式声明 UseTasksReturn 接口,使得任何使用该 Hook 的地方都能获得 tasksloadingerror 以及所有操作方法的完整类型提示,错误处理统一用 error: string | null 表达。

严格模式配置

启用 strict 获得最完整的类型检查。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,

"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src"]
}

类型设计最佳实践

  • 类型优先:先定义类型,再编写实现代码
  • 接口 vs 类型:对象类型用接口,联合类型用类型别名
  • 分层架构:类型、服务、组件分层组织
  • 输入输出分离CreateTaskInputUpdateTaskInputTask 各司其职
  • 泛型复用ApiResponse<T>PaginatedResponse<T> 用泛型统一响应结构
  • 可选字段:用 ? 表达可选,用联合类型 T | null | undefined 表达可能缺失
  • 字面量联合TaskStatusHttpMethod 用字面量联合替代枚举,更利于 Tree Shaking

类型定义是 TypeScript 项目的基础,要认真设计。多参与实际项目,在实践中加深对 TypeScript 的理解。