📚 TypeScript 教程系列

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

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

命名空间

⚠️ 不推荐使用:命名空间(namespace)是 TypeScript 早期的代码组织方案,基于全局对象,无法被现代打包工具进行 Tree Shaking,也不支持按需加载。在现代 TypeScript 开发中已不推荐使用,新项目应一律使用 ES Module(import/export)。命名空间仅保留用于维护遗留代码或与某些全局库(如早期的 jQuery、D3 等)交互。下方内容主要出于理解旧代码的目的而保留。

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

语法示意

namespacenamespace 关键字定义,内部成员需 export 才能被外部访问,外部通过点号 . 访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Drawing {
export interface IShape {
draw(): void;
}
export class Circle implements IShape {
draw() {
console.log("Circle is drawn");
}
}
}

// 通过点号访问命名空间内的成员
new Drawing.Circle().draw();

命名空间也支持嵌套(内层用 export namespace),并可跨文件拆分后用三斜杠指令 /// <reference path="..." /> 串联编译。但这些都是全局对象时代的做法:无法被现代打包器 Tree Shaking,不支持按需加载,且依赖文件加载顺序。遇到旧代码里的 namespace 理解即可;编写新代码请一律使用 ES Module(import/export)。

模块

模块系统是现代 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 const 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)。

TypeScript 5.3 起支持 import attributes 语法,导入 JSON 等非 JS 模块时可显式声明类型:import data from './config.json' with { type: 'json' },替代旧的内联断言写法 assert { type: 'json' }

默认导出

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

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
// 动态导入 - 懒加载
// import() 返回一个 Promise
async function loadMath() {
const math = await import("./math");

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

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

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

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

运行结果:

1
动态加法: 3

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

TS 5.3 起,导入非 JS 资源时可附带 import attributes 声明模块类型,最常见的是导入 JSON:

1
2
3
4
5
6
// 静态导入 JSON(需开启 resolveJsonModule)
import data from "./config.json" with { type: "json" };

// 动态导入 JSON
const mod = await import("./config.json", { with: { type: "json" } });
console.log(mod.default);

import attributes 让运行时按声明的类型正确解析模块,避免 MIME 猜测问题。

模块解析策略

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
{
"compilerOptions": {
// bundler:适用于 Vite/Webpack/esbuild 等打包工具场景(TS 5.0 起推荐)
"moduleResolution": "bundler",

// nodenext:Node.js 项目遵循 Node 的现代 ESM/CJS 解析规则
// "moduleResolution": "nodenext",

// node10(旧名 node):兼容旧版 Node.js 的经典解析策略,仅遗留项目使用
// "moduleResolution": "node10",

// classic:TypeScript 早期策略,已不推荐使用

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

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

新项目应根据运行环境选择:浏览器/打包工具场景用 bundler(配合 --module preserveESNext),Node.js 项目用 nodenext(自动判定 ESM/CJS)。旧版 node(即 node10)仅用于遗留项目,classic 已废弃。

此外,TS 5.7 新增 --rewriteRelativeImportExtensions(在 NodeNext ESM 场景下将相对导入的 .ts 扩展名重写为 .js)与 --noUncheckedSideEffectImports(对 import './polyfills' 这类副作用导入放宽未解析检查),可按需启用。

命名空间与模块的选择

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

  • 命名空间:基于全局对象,适合遗留项目或简单的脚本组织,通过三斜杠指令引用文件。
  • 模块:基于文件作用域,是现代标准,支持 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 const 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 包即可使用。

完整实例:为 npm 库编写声明文件

现代库通常把类型声明(.d.ts)随包发布,并在 package.jsontypes/exports 字段中声明,使用者 import 后即可获得类型提示,无需手动配置。下面模拟一个发布到 npm 的计算库 @demo/calc

库的实现与类型声明

库源码用 TypeScript 编写,构建时同时产出 .js.d.ts

1
2
3
4
5
6
7
8
9
// src/index.ts —— 库源码(编译后产出 index.js 与 index.d.ts)
export class Calc {
/** 计算从 0 到 limit 的所有整数之和:limit=10 → 0+1+…+10 = 55 */
doSum(limit: number): number {
let sum = 0;
for (let i = 0; i <= limit; i++) sum += i;
return sum;
}
}

如果库本身是纯 JavaScript(或不想发布 TS 产物),可以手写一份 .d.ts 放在包内,并通过 package.json 指向它:

1
2
3
4
// index.d.ts —— 手写的类型声明(只有签名,无实现)
export declare class Calc {
doSum(limit: number): number;
}

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

在 package.json 中声明类型入口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"name": "@demo/calc",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
}
}
}

exports 中的 types 条件是 moduleResolution: "nodenext"/"bundler" 解析类型入口的现代方式;顶层 types 字段则是兜底。

在业务项目中使用

安装后直接 import,TypeScript 会自动读取包内的 .d.ts

1
npm install @demo/calc
1
2
3
4
5
6
// app.ts
import { Calc } from "@demo/calc";

const calc = new Calc();
// calc.doSum("Hello"); // 编译错误:Argument of type 'string' is not assignable to 'number'
console.log(calc.doSum(10)); // 55

声明文件中的类型信息只在编译期起作用,编译产物里只剩纯 JavaScript 调用:

1
2
3
import { Calc } from "@demo/calc";
const calc = new Calc();
console.log(calc.doSum(10)); // 55

整个流程可概括为三步:分析库暴露的 API → 编写或生成 .d.ts(或随包发布) → 在 package.json 声明 types/exports,使用者 import 即享类型保护。

第三方类型

对于日常开发中常用的第三方库,绝大多数已经有现成的声明文件,通过 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 代码中引用声明文件,即可享受完整的类型检查保护。

装饰器

装饰器(Decorators)是 TypeScript 的元编程特性。从 TypeScript 5.0 起,标准化提案进入 Stage 3,装饰器语法已稳定并可直接使用,无需依赖 experimentalDecorators 标志(旧版实验性装饰器仍保留以兼容现有框架)。它让开发者能够在不修改原类的情况下,为类、方法、属性或参数增添额外的功能。装饰器本质上是一个函数,可在运行时被调用来修改目标对象的行为。

装饰器类型与应用位置

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

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

Stage 3 装饰器自 TS 5.0 起无需额外标志即可使用;若使用旧版实验性装饰器(与 Angular、TypeORM、NestJS 等框架兼容),仍需在 tsconfig.json 中启用 experimentalDecorators

启用装饰器

TypeScript 5.0 起原生支持 ECMAScript Stage 3 标准装饰器,默认开启,无需任何配置

1
2
3
4
5
6
{
"compilerOptions": {
// 标准装饰器开箱即用,无需 experimentalDecorators
"target": "ES2022"
}
}

仅当需要维护依赖实验装饰器(legacy experimental decorators)的遗留代码时,才显式开启以下选项:

1
2
3
4
5
6
7
8
{
"compilerOptions": {
// 旧版实验性装饰器:Angular/TypeORM/NestJS 等框架仍依赖
"experimentalDecorators": true,
// 生成设计时元数据,配合 reflect-metadata 供依赖注入框架使用
"emitDecoratorMetadata": true
}
}
  • 标准装饰器(默认,推荐):TS 5.0 起标准化,基于 context 对象,无需标志,是新项目首选。
  • experimentalDecorators:启用旧版实验性装饰器,主流 DI 框架(Angular、TypeORM、NestJS)目前仍依赖它。
  • emitDecoratorMetadata:仅与 experimentalDecorators 搭配生效,配合 reflect-metadata 为 DI 框架提供参数类型元数据。

下文示例统一采用 Stage 3 标准装饰器写法。标准与实验装饰器在 context 对象、metadata 语义及参数装饰器支持上存在差异,二者不要混用。

类装饰器

类装饰器作用于类本身。标准装饰器签名为 (target, context: ClassDecoratorContext)target 是被装饰的类(构造函数),context 提供类名与 metadata 等;可返回一个新类来替换原类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// Standard class decorator (TS 5.0+)
function sealed<T extends new (...args: any[]) => any>(
target: T,
context: ClassDecoratorContext
) {
console.log("装饰器 applied to: " + String(context.name));
Object.seal(target);
Object.seal(target.prototype);
return target; // 返回原类;也可返回子类替换
}

@sealed
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
}

const person = new Person("RUNOOB");
console.log("创建: " + person.name);

运行结果:

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

context.metadata 是一个在同类所有装饰器间共享的普通对象,TS 5.2 起还会自动挂载到 Class[Symbol.metadata],便于框架在类上登记元数据:

1
2
3
4
5
6
7
8
9
10
11
// 通过 context.metadata 登记路由路径
function route(path: string) {
return function (target: any, context: ClassDecoratorContext) {
context.metadata.path = path;
};
}

@route("/users")
class UserController {}

console.log((UserController as any)[Symbol.metadata]?.path); // /users

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

方法装饰器

方法装饰器作用于类的方法。标准装饰器签名为 (method, context: ClassMethodDecoratorContext)method 是原始方法函数本身(而非属性描述符),context.kind"method"context.name 为方法名。返回一个新函数即可替换原方法,不返回则保留原方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// Standard method decorator (TS 5.0+)
// 返回新函数替换原方法,实现结果缓存
function memoize(method: any, context: ClassMethodDecoratorContext) {
const cache = new Map<string, any>();
return function (this: any, ...args: any[]) {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key);
const result = method.apply(this, args);
cache.set(key, result);
return result;
};
}

class Calculator {
@memoize
square(n: number): number {
console.log(`计算 ${n} 的平方`);
return n * n;
}
}

const calc = new Calculator();
console.log(calc.square(4));
console.log(calc.square(4)); // 命中缓存,不再重复计算

运行结果:

1
2
3
计算 4 的平方
16
16

标准方法装饰器通过“返回新函数”替换原方法,无需像实验装饰器那样操作 PropertyDescriptorcontext.access 还提供 { get, set } 用于跨实例读写该属性。

访问器装饰器

访问器装饰器作用于 getter 或 setter。标准装饰器签名为 (target, context)context.kind"getter""setter"target 是原始访问器函数,返回新函数可替换之。

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
// Standard getter decorator (TS 5.0+)
// 首次访问后缓存计算结果
function once(target: any, context: ClassGetterDecoratorContext) {
let cached = false;
let value: any;
return function (this: any) {
if (!cached) {
value = target.call(this);
cached = true;
}
return value;
};
}

class Config {
// 计算开销较大,首次访问后缓存
@once
get settings() {
console.log("加载配置...");
return { theme: "dark", lang: "zh" };
}
}

const config = new Config();
console.log(config.settings.theme);
console.log(config.settings.theme); // 命中缓存

运行结果:

1
2
3
加载配置...
dark
dark

访问器装饰器可分别作用于 getter 或 setter,context.kind 用于区分二者。

属性装饰器

属性(字段)装饰器作用于类字段。标准装饰器签名为 (target, context: ClassFieldDecoratorContext),其中 target 恒为 undefinedcontext.kind"field"。字段装饰器返回一个初始化器函数 (initialValue) => newValue,用于转换字段的初始值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Standard field decorator (TS 5.0+)
// 返回初始化器,将字符串字段初始值转为大写
function upper(target: undefined, context: ClassFieldDecoratorContext) {
return function (initialValue: unknown) {
return typeof initialValue === "string"
? initialValue.toUpperCase()
: initialValue;
};
}

class User {
@upper
name: string = "alice";
}

const user = new User();
console.log("姓名: " + user.name);

运行结果:

1
姓名: ALICE

字段装饰器在定义时尚未拿到字段值,因此通过返回初始化器来介入赋值。context.metadata 同样可用于登记字段元数据。

参数装饰器

⚠️ 标准装饰器不支持参数装饰器:ECMAScript Stage 3 提案未包含参数装饰器,TypeScript 5.0 的标准装饰器会对方法参数上的 @ 报错(“A decorator can only decorate a class, method, accessor, or property.”)。参数装饰器是旧版实验装饰器专属特性,Angular、TypeORM、NestJS 等框架借助它实现依赖注入与参数校验。

若需维护依赖实验装饰器的遗留代码,需开启 experimentalDecorators: true,其签名为 (target, propertyKey, parameterIndex)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 实验装饰器专属:参数装饰器(需 experimentalDecorators: true)
function logParameter(
target: any,
propertyKey: string,
parameterIndex: number
) {
console.log("参数装饰器: " + propertyKey +
" 第 " + (parameterIndex + 1) + " 个参数");
}

class Greeter {
constructor(private greeting: string) {}

greet(@logParameter name: string) {
return this.greeting + ", " + name;
}
}

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

运行结果:

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

新项目若不依赖实验装饰器框架,应避免参数装饰器,改用方法装饰器在方法入口处统一校验或登记参数。

装饰器工厂

装饰器工厂是返回装饰器函数的函数,用于在应用装饰器时传入自定义参数。标准装饰器工厂返回的同样是 (target/method, context) 形式的标准装饰器。

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
// Standard decorator factory (TS 5.0+)
// 接收颜色码,返回标准方法装饰器
function color(colorCode: string) {
// colorCode 为 ANSI 颜色码:34=蓝 31=红 32=绿
return function (method: any, context: ClassMethodDecoratorContext) {
return function (this: any, ...args: any[]) {
const result = method.apply(this, args);
// \x1b[颜色码m 内容 \x1b[0m
return "\x1b[" + colorCode + "m" + result + "\x1b[0m";
};
};
}

class Logger {
@color("34")
log(message: string): string {
return message;
}

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

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

const 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, context: ClassDecoratorContext) {
console.log("first 应用");
};
}

function second() {
console.log("second() 求值");
return function (target: any, context: ClassDecoratorContext) {
console.log("second 应用");
};
}

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

const obj = new MyClass();

运行结果:

1
2
3
4
first() 求值
second() 求值
second 应用
first 应用

装饰器的求值顺序为:工厂表达式自上而下求值first() 先于 second()),装饰器函数自下而上应用second 先于 first)。标准与实验装饰器的求值顺序一致,真正的差异在于 context API、metadata 语义及是否支持参数装饰器,二者不应混用。

实际应用场景

日志记录

自动记录方法调用日志。

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
// Standard method decorator: 记录方法调用
function log(method: any, context: ClassMethodDecoratorContext) {
const name = String(context.name);
return function (this: any, ...args: any[]) {
console.log("调用方法: " + name + ",参数: " + JSON.stringify(args));
const result = method.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;
}
}

const 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
// 模拟当前用户角色
let currentUser = { role: "admin" };

// Standard decorator factory: 方法级权限检查
function requireRole(role: string) {
return function (method: any, context: ClassMethodDecoratorContext) {
const name = String(context.name);
return function (this: any, ...args: any[]) {
if (currentUser.role !== role) {
console.log("权限不足,无法执行 " + name);
return null;
}
return method.apply(this, args);
};
};
}

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

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

const 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 以获得运行时支持;新项目主推 Vite(见下节)。

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 主要用于遗留或大型存量项目;新项目建议直接用 Vite 的 resolve.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": "ES2022",
"module": "ESNext",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"moduleResolution": "bundler",
"resolveJsonModule": true,
"verbatimModuleSyntax": 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": "^19.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
22
{
"compilerOptions": {
// 初始阶段:宽松配置
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler", // 现代项目;Node 原生运行可改 "nodenext"
"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());
}

TS 5.5 起,JSDoc 支持 @import 指令,可在 .js 文件中直接复用 .ts 的类型,避免重复定义:

1
2
3
4
5
6
7
8
9
10
// utils.js
/** @import { User } from './types' */

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

@import 让渐进式迁移中的 JS 文件能直接引用已有的 TS 类型,无需 @typedef 重复声明。

类型声明文件

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

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 const 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-expect-error:忽略错误,且若该行实际无错则编译失败(推荐);旧写法 // @ts-ignore 会静默忽略,无法保证错误被真正消除
1
2
3
4
// legacy.js
// @ts-check
// @ts-expect-error
const result = someLegacyFunction();

最佳实践

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

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

单元测试

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

单元测试原则

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

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

测试框架配置

现代 TypeScript 项目中,测试框架的选择与工具链相关:

  • Vitest(推荐):Vite 项目的首选,基于 esbuild 原生转译 TypeScript,零额外配置,API 与 Jest 高度兼容(describe/it/expect)。
  • Bun test:Bun 运行时内置,零配置即可运行 .ts 测试,适合 Bun 项目(详见本系列第 11 篇)。
  • Jest:传统选择,生态成熟;但其 ts-jest 预处理器转译较慢,大型项目建议改用 @swc/jest 加速。

安装与配置 Vitest

1
npm install --save-dev vitest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vitest.config.ts
import { defineConfig } from 'vitest/config';

export default defineConfig({
test: {
environment: 'node', // 测试环境:node / jsdom / happy-dom
include: ['src/**/*.{test,spec}.ts'],
coverage: {
provider: 'v8',
include: ['src/**/*.ts'],
exclude: ['src/**/*.d.ts']
}
}
});

Vitest 复用 Vite 的转译管线,无需 ts-jest;Mock 用 vi.fn()/vi.mock()(对应 Jest 的 jest.fn()/jest.mock())。

Jest(传统选择)

若项目仍用 Jest,推荐以 @swc/jest 替代 ts-jest 提速:

1
npm install --save-dev jest @swc/jest @types/jest
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// jest.config.js
module.exports = {
transform: {
'^.+\.ts$': '@swc/jest', // 用 SWC 转译,比 ts-jest 快很多
},
testEnvironment: 'node',
roots: ['<rootDir>/src'],
testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
moduleFileExtensions: ['ts', 'js', 'json'],
collectCoverageFrom: [
'src/**/*.ts',
'!src/**/*.d.ts' // 排除类型声明文件
]
};

测试文件通常放在 __tests__ 目录或以 .test.ts/.spec.ts 结尾。下文示例使用 describe/it/expect,这套 API 在 Vitest 与 Jest 中一致。

测试函数

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

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% 以上。

TypeScript 5.5 起提供 --isolatedDeclarations,强制每个文件可被独立生成声明文件,便于工具并行处理;5.6 起新增 --noCheck(跳过类型检查仅做语法转换,适合在 CI 中将类型检查与构建分离)和 --erasableSyntaxOnly(禁止 emit 阶段无法擦除的语法,配合 Node.js 原生类型擦除运行)。这些选项可在大型项目中进一步优化编译与运行流程。

项目引用优化

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

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']
}
}
},

// 压缩代码:Vite 默认用 esbuild(快);如需更极致压缩可改 'terser'(需额外安装、更慢)
minify: 'esbuild',

// 生成 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
// 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;
}

// 具名导出更利于按需导入与可读性
// 现代打包器(esbuild/Rollup/Vite)对具名导出与默认导出均能 Tree Shaking
// export default { add, subtract, multiply, divide };

console.log("工具模块加载");

现代打包器(esbuild/Rollup/Vite)对具名导出与默认导出都能做 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": "ES2022",
"useDefineForClassFields": true,
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"module": "ESNext",
"skipLibCheck": true,

"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"verbatimModuleSyntax": 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 的理解。