📚 TypeScript 教程系列
入门与配置 基础类型与变量声明 函数 流程控制与运算符 集合类型 异步编程与错误处理 接口与类 泛型与类型组合 高级类型 模块、装饰器与工程化 (本文)⚠️ 来源声明 :本文内容参考自 菜鸟教程 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 文件中,则需要用三斜杠指令引用它:
多文件实例 IShape.ts 文件:
1 2 3 4 5 namespace Drawing { export interface IShape { draw (); } }
Circle.ts 文件:
1 2 3 4 5 6 7 8 namespace Drawing { export class Circle implements IShape { public draw ( ) { console .log ("Circle is drawn" ); } } }
Triangle.ts 文件:
1 2 3 4 5 6 7 8 namespace Drawing { export class Triangle implements IShape { public draw ( ) { console .log ("Triangle is drawn" ); } } }
TestShape.ts 文件:
1 2 3 4 5 6 7 8 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 var invoice = new Runoob .invoiceApp .Invoice ();console .log (invoice.calculateDiscount (500 ));
编译命令:tsc --out app.js InvoiceTest.ts,运行输出:
模块 模块系统是现代 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 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 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, WorldHello, TypeScript
导入路径可以是相对路径(如 ./user)或绝对路径(如 @/utils)。
默认导出 每个模块可以有一个默认导出。默认导出在导入时不需要使用花括号,且可以取任意名字。
1 2 3 4 5 6 7 8 9 10 11 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 import add from "./math" ;import { multiply } from "./math" ;console .log ("加法: " + add (2 , 3 ));console .log ("乘法: " + multiply (4 , 5 ));
运行结果:
对于工具函数、类等主要导出内容使用默认导出,对于辅助函数、接口等使用命名导出。
重新导出 重新导出(Re-export)用于聚合多个模块的内容,或将一个模块的导出暴露给另一个模块。
1 2 3 4 5 6 7 8 9 10 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 async function loadMath ( ) { var math = await import ("./math" ); console .log ("动态加法: " + math.default (1 , 2 )); }loadMath ();async function loadFeature (enable : boolean ) { if (enable) { var feature = await import ("./feature" ); feature.run (); } }loadFeature (true );
运行结果:
动态导入可以实现代码分割,只在需要时加载额外的代码,减少初始加载时间。
模块解析策略 TypeScript 提供了多种模块解析策略,用于查找导入的模块,可以在 tsconfig.json 中配置。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 { "compilerOptions" : { "moduleResolution" : "node" , "moduleResolution" : "classic" , "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 文件中会报错:
报错原因是 TypeScript 不认识 $ 和 jQuery。TypeScript 在编译阶段就需要知道每个变量和函数的类型,而 jQuery 作为纯 JavaScript 库不包含任何类型信息。
快速修复:declare 关键字 最简单的方式是用 declare 关键字手动告知 TypeScript 某个变量的存在及其类型:
1 2 declare var jQuery : (selector : string ) => any ;jQuery ('#foo' );
这段代码的含义是:声明变量 jQuery 为一个函数,接收 string 类型参数,返回 any 类型。declare 关键字声明的类型只在编译阶段起作用,编译后的 JavaScript 代码中会被完全删除,不会影响运行时的行为。编译后的 JavaScript 代码为:
但 declare 只能解决单个文件的临时问题。对于包含许多方法和类的库,在每个文件中手写 declare 不现实,因此需要声明文件。
声明文件:一劳永逸的方案 声明文件将所有 declare 声明集中放到一个独立文件中,项目中的任何 TypeScript 文件都可以引用它。
文件命名规范 声明文件统一以 .d.ts 为后缀,d 代表 declaration(声明)。例如 runoob.d.ts。
基本语法 声明一个模块的语法如下:
1 2 declare module Module _Name { }
在 TypeScript 文件中通过三斜线指令引入声明文件:
三斜线指令是 TypeScript 特有的语法,用于告知编译器在编译时需要包含指定的声明文件。很多流行第三方库(如 jQuery、Lodash)的声明文件已经由社区维护好了,存放在 DefinitelyTyped 项目中,通过 npm 安装对应的 @types/xxx 包即可使用。
完整实例:从零创建声明文件 整个流程涉及以下文件:
文件 作用 CalcThirdPartyJsLib.js 第三方 JavaScript 库(纯 JS,无类型信息) Calc.d.ts 声明文件(手动编写,描述库的类型) CalcTest.ts TypeScript 业务代码(引用声明文件,调用库) 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 var Runoob ; (function (Runoob ) { var Calc = (function ( ) { function Calc ( ) { } }) Calc .prototype .doSum = function (limit ) { var sum = 0 ; for (var i = 0 ; i <= limit; i++) { sum = sum + i; } return sum; } 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 declare module Runoob { export class Calc { doSum (limit : number ): number ; } }
声明文件和普通 .ts 文件的最大区别:声明文件只有类型签名,没有实现代码。
第三步:在 TypeScript 代码中使用 1 2 3 4 5 6 7 8 9 var obj = new Runoob .Calc ();console .log (obj.doSum (10 ));
被注释的那一行如果取消注释,编译时会直接报错,因为声明文件中规定 doSum 只接受 number 类型参数。
第四步:编译 TypeScript 使用 tsc 命令编译:
编译后生成的 CalcTest.js 内容如下:
1 2 3 4 5 var obj = new Runoob .Calc ();console .log (obj.doSum (10 ));
三斜线指令和声明文件中的类型信息在编译产物中都不见了,只剩下纯粹的 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 > <script src = "CalcThirdPartyJsLib.js" > </script > <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 库。整个工作流程可概括为三步:
拿到一个 JS 库,分析它暴露了哪些 API。 编写 .d.ts 声明文件,描述这些 API 的类型签名。 在 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 function sealed (target : Function ) { console .log ("装饰器 applied to: " + target.name ); 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 );
运行结果:
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 ) { 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" );console .log ("方法可枚举: " + g.propertyIsEnumerable ("greet" ));for (var key in g) { console .log ("属性: " + key); }
运行结果:
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 ) { descriptor.configurable = value; }; }class Point { private _x : number = 0 ; private _y : number = 0 ; @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 ) { 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 ) { 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 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 ) { return function ( target : any , propertyKey : string , descriptor : PropertyDescriptor ) { var originalMethod = descriptor.value ; descriptor.value = function (...args : any [] ) { var result = originalMethod.apply (this , args); 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; } }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 中配置 paths 和 baseUrl。
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 const path = require ('path' );module .exports = { 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' ) }, 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 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 { "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 { "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 import { formatDate, formatCurrency } from '@my-utils/format' ;import { Button , Modal , Input } from '@my-ui/core' ;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.json、tsconfig.json、lerna.json、turbo.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 { "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 { "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 { "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 { "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 { "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 { "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,核心策略是渐进式迁移。
迁移策略 添加 tsconfig.json 重命名 .js 为 .ts 逐步添加类型注解 启用严格模式 配置 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 , "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 { "compilerOptions" : { "strict" : false , "noImplicitAny" : false } } { "compilerOptions" : { "strict" : true , "noImplicitAny" : true , "strictNullChecks" : true } } { "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 function add (a, b ) { return a + b; }function getUser (id ) { return fetch (`/api/users/${id} ` ).then (r => r.json ()); }
类型声明文件 为没有类型定义的模块创建声明。
1 2 3 4 5 6 7 8 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 var result = someLegacyFunction ();
最佳实践 从关键模块开始迁移 添加单元测试 逐步启用严格模式 使用 JSDoc 注释 创建类型声明文件 迁移的核心是渐进式:逐步迁移、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 = { preset : 'ts-jest' , 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 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 import { Calculator } from "./calculator" ;describe ("Calculator" , () => { let 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 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 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 const mockCallback = jest.fn (x => x * 2 ); [1 , 2 , 3 ].forEach (mockCallback);expect (mockCallback).toHaveBeenCalledTimes (3 );expect (mockCallback).toHaveBeenCalledWith (2 ); 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、CommandTypeScript 特有模式 :类型安全的依赖注入、泛型工厂、条件类型选择设计模式是软件设计中常见问题的可重用解决方案,是代码设计经验的总结。
单例模式(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 , "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 { "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 ;function add (a : number , b : number ) { return a + b; }const user = { id : 1 , name : "Bob" , email : "bob@example.com" };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 function processData (data : any ): any { return data.value ; }function processData<T extends { value : string }>(data : T): string { return data.value ; }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 PartialUser = Partial <User >;type ReadonlyUser = Readonly <User >;interface Point { x : number ; y : number ; }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 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 : 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 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; }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 export type TaskStatus = "pending" | "in-progress" | "completed" ;export type TaskPriority = "low" | "medium" | "high" ;export interface Task { id : string ; 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 不包含 id、status、createdAt 等由系统生成的字段,而 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 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 >; }export type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE" ;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 import { Task , CreateTaskInput , UpdateTaskInput , TaskFilter , TaskStatus } from "../types/task" ;class TaskService { getAll (filter ?: TaskFilter ): Task [] { return []; } getById (id : string ): Task | undefined { return undefined ; } create (input : CreateTaskInput ): Task { return {} as Task ; } update (id : string , input : UpdateTaskInput ): Task | null { return null ; } delete (id : string ): boolean { return false ; } 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 import { Task , CreateTaskInput , UpdateTaskInput , TaskFilter , TaskStatus } from "../types/task" ;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 的地方都能获得 tasks、loading、error 以及所有操作方法的完整类型提示,错误处理统一用 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 类型 :对象类型用接口,联合类型用类型别名分层架构 :类型、服务、组件分层组织输入输出分离 :CreateTaskInput、UpdateTaskInput、Task 各司其职泛型复用 :ApiResponse<T>、PaginatedResponse<T> 用泛型统一响应结构可选字段 :用 ? 表达可选,用联合类型 T | null | undefined 表达可能缺失字面量联合 :TaskStatus、HttpMethod 用字面量联合替代枚举,更利于 Tree Shaking类型定义是 TypeScript 项目的基础,要认真设计。多参与实际项目,在实践中加深对 TypeScript 的理解。