一、JavaScript vs TypeScript 详细对比表
| 特性 / 维度 | JavaScript | TypeScript |
|---|
| 类型系统 | 动态类型(运行时检查) | 静态类型(编译时检查) |
| 类型注解 | 无 | 支持 : type 语法 |
| 类型推断 | 有限 | 强大,可自动推导类型 |
| 接口(Interface) | 无 | 支持 |
| 泛型(Generics) | 无 | 支持 |
| 枚举(Enum) | 无(需用对象模拟) | 原生支持 |
| 类继承增强 | 基础继承 | 支持 public / private / protected 等修饰符 |
| 编译步骤 | 直接运行 | 需要编译为 JS |
| 错误发现 | 运行时 | 编译时 |
| 工具支持 | 一般 | 极佳(智能提示、重构) |
| 学习曲线 | 低 | 中等(需学类型系统) |
| 适用场景 | 小型项目、脚本、原型 | 中大型项目、团队协作 |
二、TypeScript 类型系统
1. 类型基础
基本类型: string、number、boolean、null、undefined、symbol、bigint
| 类型 | 作用 | 代码示例 | 注意事项 |
|---|
string | 表示文本类型,用于字符串值 | let name: string = "Alice"; | 区分大小写;可使用单引号、双引号或模板字符串;不可赋值为 null 或 undefined(除非启用 strictNullChecks: false) |
number | 表示所有数字类型(浮点、整数、NaN、Infinity) | let age: number = 25; let price: number = 9.99; | TypeScript 中所有数字都是 number 类型(无 int/float 区分);支持二进制、八进制、十六进制字面量 |
boolean | 表示布尔值:true 或 false | let isActive: boolean = true; | 不能将 0、1、"true" 等非布尔值赋给 boolean 类型变量 |
null | 表示”无值”,是一个明确的空值 | let data: null = null; | 默认情况下 null 是所有类型的子类型(除非启用 strictNullChecks: true);通常用于显式清空变量 |
undefined | 表示”未定义”,变量声明但未初始化 | let name: undefined = undefined; let x; // x 的值为 undefined | 函数无返回值时返回 undefined;启用 strictNullChecks 后,不能赋给其他类型(如 string) |
symbol | 表示唯一且不可变的值,常用于对象属性键 | const key1 = Symbol("id"); const key2 = Symbol("id"); console.log(key1 === key2); // false | 每个 Symbol() 调用返回唯一值;不能与其他类型进行运算或隐式转换;用作对象属性时可避免命名冲突 |
bigint | 表示任意精度的整数,用于超出 Number.MAX_SAFE_INTEGER 的大整数 | let bigNum: bigint = 123n; let another = BigInt(100); | 必须以 n 结尾或使用 BigInt() 构造;不能与 number 类型混合运算;不支持小数 |
类型注解与类型推断
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 类型注解 (Type Annotation) | 显式指定变量、函数参数或返回值的类型 | let count: number = 10; function greet(name: string): string { return "Hello, " + name; } | 提高代码可读性和类型安全;推荐在函数参数和返回值上使用;不要过度标注,让类型推断发挥作用 |
| 类型推断 (Type Inference) | TypeScript 自动根据赋值推断变量类型 | let message = "Hello"; // 推断为 string let count = 42; // 推断为 number let items = [1, 2, 3]; // 推断为 number[] | 初始赋值决定类型;数组混合类型会推断为联合类型(如 [1, "a"] → (number | string)[]);对于空数组或复杂对象,建议手动标注 |
| 联合类型推断 | 当值可能为多种类型时,TS 推断为联合类型 | let val = Math.random() > 0.5 ? "yes" : 42; // val: string | number | 后续使用需进行类型收窄(如 typeof 判断);否则无法调用特定类型的方法 |
any、unknown、never 的区别与使用场景
| 类型 | 作用 | 代码示例 | 注意事项 |
|---|
any | 禁用类型检查,允许任意操作 | let data: any = "hello"; data = 100; data.toFixed(); // 不报错,但可能运行时报错 | 尽量避免使用,破坏类型安全;适用于迁移旧 JS 项目或第三方库无类型定义时;可赋值给任何类型,也可被任何类型赋值 |
unknown | 表示未知类型,比 any 更安全 | let input: unknown; input = "hello"; input = 100; // OK // input.toFixed(); // ❌ 编译错误,必须先类型收窄 if (typeof input === "number") { console.log(input.toFixed(2)); } | 是 any 的类型安全替代方案;不能直接操作,必须通过类型守卫(type guard)收窄后使用;推荐用于函数参数、API 返回值等不确定类型场景 |
never | 表示永远不会发生的类型,常用于抛出异常或无限循环的函数返回值 | function throwError(message: string): never { throw new Error(message); } function infiniteLoop(): never { while (true) {} } | never 是所有类型的子类型(可赋值给任何类型);但没有类型是 never 的子类型(反之不行);用于类型收窄的穷尽性检查(exhaustive check) |
使用建议:
- 优先使用
unknown 替代 any
never 多用于工具类型和控制流分析
类型断言:as 与 <Type> 语法
| 语法 | 作用 | 代码示例 | 注意事项 |
|---|
as Type(推荐) | 将值断言为某个特定类型 | const input = document.getElementById("name") as HTMLInputElement; input.value = "Alice"; | JSX 中只能使用 as 语法(<Type> 会被误认为 JSX 标签);不进行运行时检查,仅告诉编译器”我确定它是这个类型”;错误断言可能导致运行时错误 |
<Type>(旧语法) | 同上,泛型风格语法 | const input = <HTMLInputElement>document.getElementById("name"); input.value = "Bob"; | 在 .ts 文件中可用,但在 .tsx 中不支持;建议统一使用 as 语法以保持一致性 |
非空断言 ! | 断言某个值不为 null 或 undefined | let el: HTMLElement | null = document.getElementById("app"); el!.innerHTML = "Hello"; // 不检查是否为 null | 仅在确定值存在时使用;否则可能导致 Cannot read property 'xxx' of null |
| 双重断言(不推荐) | 强制转换类型(先转 any / unknown,再转目标类型) | let a = "hello" as any as number; // 危险! | 绕过类型系统,极易出错;仅在极端兼容场景下使用,应避免 |
⚠️ 类型断言本质是”类型欺骗”,应谨慎使用,优先考虑类型守卫(如 if (x instanceof Date))或联合类型处理。
总结建议:
| 最佳实践 | 说明 |
|---|
✅ 使用 unknown 替代 any | 提高类型安全性 |
✅ 启用 strictNullChecks | 避免 null / undefined 意外赋值 |
✅ 优先使用 as Type 断言语法 | 兼容 JSX,代码更清晰 |
| ✅ 避免过度使用类型断言 | 会削弱类型系统的保护作用 |
| ✅ 利用类型推断减少冗余注解 | 保持代码简洁 |
2. 复杂类型
数组与元组(Tuple)
| 类型 | 作用 | 代码示例 | 注意事项 |
|---|
| 数组(Array) | 表示相同类型元素的有序集合 | let numbers: number[] = [1, 2, 3]; let words: Array<string> = ["a", "b"]; // 泛型写法 | 所有元素必须是同一类型(或联合类型);可使用 push、map 等方法;Array<T> 与 T[] 等价 |
| 元组(Tuple) | 表示固定长度、类型有序的数组,每个位置可有不同类型 | let user: [string, number, boolean] = ["Alice", 25, true]; // [name, age, isActive] | 长度和类型顺序固定;访问越界索引可能返回 undefined 或报错(取决于配置);支持只读元组:readonly [string, number] |
| 越界与可变性 | 元组在 JS 中仍是数组,可 push,但 TS 不检查运行时行为 | user.push("admin"); // 合法(JS 层面) // user[3] 类型为 string | number | boolean user[1] = 30; // OK user[3] = "extra"; // ❌ 编译错误(若严格模式) | TS 仅在编译时检查元组类型;push 后访问新元素会推断为联合类型;推荐配合 readonly 使用防止修改 |
| 命名元组(可读性增强) | 为元组元素添加标签,提升可读性 | let range: [min: number, max: number] = [0, 100]; | 标签仅用于文档和 IDE 提示,不影响类型检查;仍按索引访问(如 range[0]) |
对象类型与可选 / 只读属性
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 对象类型(匿名) | 定义对象的结构:属性名与类型 | let user: { name: string; age: number } = { name: "Bob", age: 30 }; | 属性必须完全匹配(多余或缺少属性会报错);支持嵌套对象类型 |
可选属性(?) | 表示某个属性可以不存在 | let config: { url: string; timeout?: number; // 可选 } = { url: "api.example.com" }; | 访问可选属性前应检查是否存在;类型为 T | undefined |
只读属性(readonly) | 属性只能在初始化时赋值,之后不可修改 | let point: { readonly x: number; readonly y: number; } = { x: 10, y: 20 }; // point.x = 30; // ❌ 错误 | 用于定义常量对象、配置项等;与 const 不同:const 限制变量绑定,readonly 限制属性修改 |
| 只读数组与元组 | 防止数组被修改 | let list: readonly string[] = ["a", "b"]; // list.push("c"); // ❌ 错误 let tuple: readonly [number, string] = [1, "a"]; | ReadonlyArray<T> 等价于 readonly T[];方法如 push、pop 被禁用,但 concat、slice 可用 |
联合类型(Union)与交叉类型(Intersection)
| 类型 | 作用 | 代码示例 | 注意事项 |
|---|
联合类型(|) | 表示一个值可以是多种类型之一 | function printId(id: number | string) { console.log("ID: " + id); } printId(101); // OK printId("abc"); // OK | 只能访问所有类型的共有成员(如 toString());必须使用类型守卫(typeof、in、instanceof)进行收窄才能调用特定方法 |
| 字面量联合类型 | 限制值为几个特定字面量之一 | type Direction = "left" | "right" | "up" | "down"; let dir: Direction = "left"; // dir = "forward"; // ❌ 错误 | 实现类似枚举的效果;常用于函数参数、状态机等 |
交叉类型(&) | 将多个类型合并为一个新类型(取所有类型的交集) | type Person = { name: string; age: number }; type Employee = { id: number; department: string }; type Staff = Person & Employee; let staff: Staff = { name: "Alice", age: 28, id: 1001, department: "Engineering" }; | 属性冲突时,类型为 never(如 { x: string } & { x: number });用于混入(mixin)或组合多个接口 |
| 联合 vs 交叉 | 联合是”或”,交叉是”且” | type A = { a: string }; type B = { b: number }; type U = A | B; // 有 a 或 b type I = A & B; // 同时有 a 和 b | U 类型变量必须通过类型守卫判断具体类型;I 类型变量必须包含所有属性 |
类型别名(Type Alias)
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
类型别名(type) | 为类型创建一个新名称,提高可读性和复用性 | type Point = { x: number; y: number }; type ID = string | number; type Callback = (result: boolean) => void; let p: Point = { x: 1, y: 2 }; | 可用于对象、联合、元组、函数等任意类型;不创建新类型,只是别名(等价替换) |
| 泛型类型别名 | 支持泛型参数,增强复用性 | type Box<T> = { value: T }; let stringBox: Box<string> = { value: "hello" }; let numberBox: Box<number> = { value: 42 }; | 与泛型接口类似,但更轻量;不能被 extends 或 implements |
| 联合/交叉组合 | 用于构建复杂类型结构 | type Status = "loading" | "success" | "error"; type ApiResponse<T> = { status: "loading" }; | - |
与接口(interface)对比:
- 类型别名更灵活,接口更适合扩展
- 类型别名可表示原始类型、联合等;接口支持自动合并(declaration merging)
- 一般建议:对象结构用
interface,复杂组合用 type
总结建议:
| 最佳实践 | 说明 |
|---|
✅ 使用元组时考虑 readonly | 防止意外修改 |
✅ 对象类型优先使用 interface 或 type 命名 | 提高可维护性 |
| ✅ 联合类型必须配合类型守卫使用 | 避免访问不存在的属性 |
| ✅ 交叉类型慎用于属性冲突场景 | 否则可能导致 never |
| ✅ 类型别名用于简化复杂类型 | 如联合、泛型、映射类型等 |
3. 接口(Interface)
定义对象结构
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 基本接口定义 | 描述对象的形状(shape),规定属性名和类型 | interface User { name: string; age: number; isActive: boolean; } let user: User = { name: "Alice", age: 28, isActive: true }; | 接口是”契约”,变量必须满足所有必需属性;多余属性会报错(除非使用索引签名或类型断言) |
可选属性(?) | 指定某些属性可以不存在 | interface Config { apiUrl: string; timeout?: number; // 可选 retries?: number; } let config: Config = { apiUrl: "https://api.example.com" }; // OK | 访问可选属性前应检查是否为 undefined;类型为 T | undefined |
只读属性(readonly) | 属性只能在创建时赋值,之后不可修改 | interface Point { readonly x: number; readonly y: number; } let p: Point = { x: 10, y: 20 }; // p.x = 30; // ❌ 编译错误 | 用于定义常量数据结构;与 const 不同:const 控制变量绑定,readonly 控制属性修改 |
函数类型接口
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 接口描述函数 | 使用接口定义函数的参数和返回值类型 | interface GreetFunction { (name: string): string; } let greet: GreetFunction = function(name: string): string { return "Hello, " + name; }; | 接口没有函数名,只关注调用签名;实现该接口的函数必须匹配参数类型和返回类型 |
| 多调用签名 | 一个接口支持多种函数重载形式 | interface Format { (value: string): string; (value: number, decimals?: number): string; } | 实现函数需兼容所有签名;常用于库函数的重载设计 |
| 与类型别名对比 | type 也可定义函数类型,语法更灵活 | type GreetType = (name: string) => string; interface GreetInterface { (name: string): string; } | 功能几乎等价;接口支持声明合并,类型别名不支持;一般建议:函数类型可用 type 更简洁 |
可索引类型
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 字符串索引签名 | 定义对象可通过字符串键动态访问属性 | interface StringArray { [index: number]: string; // 数字索引 → 字符串值 } let arr: StringArray = ["a", "b"]; console.log(arr[0]); // "a" interface Dictionary { [key: string]: string; } let dict: Dictionary = { name: "Bob", role: "admin" }; | 支持两种索引类型:string 和 number;若同时存在 string 和 number 索引,number 索引的返回类型必须是 string 索引的子类型 |
| 数字索引签名 | 主要用于类数组结构 | interface NumberDictionary { [index: string]: number; length: number; // OK, length 是 number // name: string; // ❌ 错误!string 不是 number 的子类型 } | JS 中数字索引会被转为字符串(obj[0] → obj["0"]);因此 number 索引签名实际上也适用字符串键 |
| 索引签名限制 | 提高安全性,防止任意属性写入 | interface SafeObject { [key: string]: string; id: string; // OK // tags: string[]; // ❌ 错误,string[] 不是 string 子类型 } | 所有明确属性必须符合索引签名的类型;否则会导致类型冲突 |
接口继承与实现
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
接口继承(extends) | 一个接口可以继承一个或多个其他接口 | interface Person { name: string; age: number; } interface Employee extends Person { employeeId: number; department: string; } let emp: Employee = { name: "Alice", age: 28, employeeId: 1001, department: "Engineering" }; | 支持多重继承:interface Admin extends Person, Permissions;继承后包含所有父接口的成员 |
类实现接口(implements) | 类必须实现接口中定义的所有成员 | interface Drawable { draw(): void; } class Circle implements Drawable { draw() { console.log("Drawing a circle"); } } | 一个类可实现多个接口:class A implements X, Y;实现是”承诺”,确保类具有指定结构 |
| 混合继承与实现 | 类可继承父类并实现接口 | class Animal { constructor(public name: string) {} } interface Flyable { fly(): void; } class Bird extends Animal implements Flyable { fly() { console.log(\${this.name} is flying`); } }` | extends 用于类继承(获取父类逻辑);implements 用于接口实现(满足契约);可同时使用 |
| 接口合并(Declaration Merging) | 同名接口会自动合并 | interface Box { value: string; } interface Box { count: number; } // 等价于: interface Box { value: string; count: number; } let b: Box = { value: "test", count: 1 }; | 是接口独有的特性,type 不支持;常用于扩展第三方库的类型定义 |
总结建议:
| 最佳实践 | 说明 |
|---|
✅ 优先使用 interface 描述对象结构 | 更符合面向对象设计 |
| ✅ 使用可选和只读属性提高类型安全性 | 明确意图,防止误改 |
| ✅ 函数类型接口适用于重载场景 | 支持多调用签名 |
| ✅ 利用接口继承构建类型层级 | 实现”is-a”关系 |
✅ implements 强制类遵循契约 | 提高代码可维护性 |
| ✅ 利用声明合并扩展类型 | 特别适合库的类型增强 |
💡 接口 vs 类型别名(interface vs type)速查表:
| 场景 | 推荐 |
|---|
| 定义对象结构 | ✅ interface |
| 需要声明合并 | ✅ interface |
| 定义联合/交叉/原始类型 | ✅ type |
| 泛型高级类型(如映射类型) | ✅ type |
| 函数类型 | type 或 interface 均可 |
| 类实现契约 | ✅ interface |
4. 类(Class)增强
访问修饰符:public / private / protected
| 修饰符 | 作用 | 代码示例 | 注意事项 |
|---|
public | 默认值,属性或方法可在任何地方访问 | class Person { public name: string; constructor(name: string) { this.name = name; } public greet() { return "Hello, " + this.name; } } const p = new Person("Alice"); console.log(p.name); // OK p.greet(); // OK | 所有成员默认为 public;可在类内部、子类、实例上访问 |
private | 仅在当前类内部可访问,子类和实例不可访问 | class BankAccount { private balance: number = 0; private updateLog(amount: number) { console.log(\Balance changed by ${amount}`); } deposit(amount: number) { this.balance += amount; this.updateLog(amount); } } const account = new BankAccount(); // account.balance; // ❌ 编译错误 // account.updateLog(100); // ❌ 错误` | TypeScript 层面限制,编译为 JS 后仍可通过 ['balance'] 访问(无运行时保护);用于封装敏感数据或内部逻辑 |
protected | 在当前类和子类中可访问,实例不可访问 | class Vehicle { protected speed: number = 0; protected startEngine() { console.log("Engine started"); } } class Car extends Vehicle { accelerate() { this.speed += 10; // OK this.startEngine(); // OK } } const car = new Car(); // car.speed; // ❌ 错误 // car.startEngine(); // ❌ 错误 | 支持继承链中的受控访问;适合设计框架或基类时使用 |
⚠️ TypeScript 的访问修饰符仅在编译时检查,JS 运行时无访问控制。如需真正私有,应使用 JS 的 # 私有字段。
只读属性与参数属性
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
readonly 属性 | 属性只能在声明或构造函数中赋值,之后不可修改 | class Person { readonly id: number; readonly name: string = "Unknown"; constructor(id: number, name: string) { this.id = id; this.name = name; // 覆盖默认值 } // someMethod() { // this.id = 100; // ❌ 错误 // } } | 类似 const,但用于类属性;常用于 ID、配置项等不可变数据 |
| 参数属性(Parameter Properties) | 在构造函数参数前加修饰符,自动生成类属性并赋值 | class Person { constructor( public name: string, private age: number, readonly id: number ) { // 自动创建并赋值:this.name, this.age, this.id } getInfo() { return \${this.name}, ${this.age}, ID: ${this.id}`; } } const p = new Person(“Alice”, 25, 1001); console.log(p.name); // OK // console.log(p.age); // ❌ private // p.id = 1002; // ❌ readonly` | 减少样板代码;支持 public、private、protected、readonly 及其组合(如 readonly private);不能重复声明同名属性 |
静态成员
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
static 属性 | 属于类本身,不依赖实例,所有实例共享 | class Counter { static count: number = 0; static MAX_COUNT = 100; constructor() { Counter.count++; } static isMax() { return Counter.count >= Counter.MAX_COUNT; } } console.log(Counter.count); // 0 new Counter(); new Counter(); console.log(Counter.count); // 2 | 通过类名访问,不能通过 this 或实例访问;常用于计数器、常量、工具方法 |
static 方法 | 静态函数,通常作为工具函数或工厂方法 | class MathUtils { static add(a: number, b: number): number { return a + b; } static createZeroPoint() { return { x: 0, y: 0 }; } } const sum = MathUtils.add(2, 3); // 5 | 不能访问实例属性或方法(无 this 上下文);适合无状态的工具函数 |
| 静态块(TypeScript 4.4+) | 在类加载时执行初始化逻辑 | - | - |
抽象类与方法
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
abstract 类 | 不能被实例化,只能被继承 | abstract class Shape { abstract area(): number; abstract perimeter(): number; logInfo() { console.log(\Area: ${this.area()}`); } } // const s = new Shape(); // ❌ 错误` | 用于定义”模板”或”契约”;强制子类实现特定行为 |
abstract 方法 | 仅定义方法签名,无实现,必须在子类中实现 | class Circle extends Shape { constructor(private radius: number) { super(); } area(): number { return Math.PI * this.radius ** 2; } perimeter(): number { return 2 * Math.PI * this.radius; } } | 抽象方法不能有方法体;子类必须实现所有抽象方法,否则也必须声明为 abstract |
| 抽象类 vs 接口 | 抽象类可包含实现和状态,接口只能定义结构 | 接口更轻量,支持多实现;抽象类适合共享代码和状态;可结合使用:类实现接口并继承抽象类 | - |
扩展:JS 原生私有字段(#)
TypeScript 也支持 JavaScript 的原生私有字段 #,提供运行时私有性:
class BankAccount {
#balance: number = 0;
deposit(amount: number) {
this.#balance += amount;
}
getBalance() {
return this.#balance;
}
}
#balance 在 JS 运行时真正私有,无法通过 obj.#balance 或反射访问
- 与
private 不兼容,不能互相访问
- 推荐用于需要强封装的场景
总结建议:
| 最佳实践 | 说明 |
|---|
✅ 使用 private 封装内部状态 | 防止外部误用 |
✅ 用 protected 设计可扩展的基类 | 允许子类访问关键逻辑 |
✅ readonly + 参数属性减少冗余代码 | 提高开发效率 |
| ✅ 静态成员用于工具函数和共享数据 | 避免实例化开销 |
| ✅ 抽象类用于定义框架或组件模板 | 强制实现关键逻辑 |
| ✅ 抽象类和接口可结合使用 | 实现”is-a”关系并共享代码 |
5. 泛型(Generics)
泛型函数与泛型类
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 泛型函数 | 函数的操作不依赖具体类型,通过类型参数保持灵活性 | function identity<T>(arg: T): T { return arg; } // 使用 let output1 = identity<string>("hello"); // 推断为 string let output2 = identity(42); // 类型推断:T = number // 泛型数组 function getFirst<T>(arr: T[]): T | undefined { return arr[0]; } getFirst([1, 2, 3]); // 返回 number | undefined | 类型参数 T 在调用时确定;可使用多个类型参数:<T, U>;尽量让 TS 自动推断类型,避免冗余标注 |
| 泛型类 | 类的某些属性或方法依赖未定类型 | class Box<T> { value: T; constructor(value: T) { this.value = value; } getValue(): T { return this.value; } setValue(value: T): void { this.value = value; } } let stringBox = new Box("hello"); // T = string let numberBox = new Box(100); // T = number | 类名后声明泛型参数 <T>;所有实例共享类逻辑,但类型独立;构造函数不能是泛型的(但类可以是) |
泛型约束
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
extends 约束 | 限制泛型参数必须满足某个类型结构 | interface Lengthwise { length: number; } function logLength<T extends Lengthwise>(arg: T): T { console.log(arg.length); // OK,因为保证有 length 属性 return arg; } logLength("hello"); // string 有 length logLength([1, 2, 3]); // 数组有 length // logLength(42); // ❌ 错误:number 没有 length | 避免对泛型做非法操作;约束越具体,可用属性越多;可结合交叉类型增强约束:T extends A & B |
| 多类型参数约束 | 多个泛型参数之间建立关系 | function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // 安全访问 } let user = { name: "Alice", age: 28 }; getProperty(user, "name"); // 返回 string // getProperty(user, "email"); // ❌ 编译错误(若 email 不存在) | keyof T 表示 T 的所有键的联合类型;K extends keyof T 确保 key 是 obj 的有效属性;这是泛型高级用法的核心模式之一 |
泛型默认值
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 默认类型参数 | 为泛型提供默认类型,提升易用性 | class Component<T = HTMLElement> { element: T; constructor(el: T) { this.element = el; } } // 使用默认类型 let div = new Component(document.getElementById("app")); // T = HTMLElement // 显式指定类型 let input = new Component<HTMLInputElement>(document.querySelector("input")); | 语法:<T = DefaultType>;提高 API 友好性,减少重复标注;常用于配置对象、容器类等场景 |
| 函数泛型默认值 | 同样适用于函数 | function createArray<T = string>(length: number, value: T): T[] { return Array(length).fill(value); } createArray(3, "hi"); // string[] createArray<number>(3, 42); // number[] createArray(2, true); // boolean[],自动推断 T = boolean | 若能推断出类型,则忽略默认值;默认值仅在无法推断时生效 |
泛型工具类型:Partial、Required、Readonly 等
| 工具类型 | 作用 | 代码示例 | 注意事项 |
|---|
Partial<T> | 将 T 的所有属性变为可选 | interface User { id: number; name: string; email: string; } function updateUser(id: number, changes: Partial<User>) { // 模拟更新用户信息 } updateUser(1, { name: "Bob" }); // OK,只改 name | 常用于”补丁”对象(如 PATCH 请求);实现原理:type Partial<T> = { [P in keyof T]?: T[P]; } |
Required<T> | 将 T 的所有属性变为必选 | interface Config { apiUrl?: string; timeout?: number; } const defaultConfig: Required<Config> = { apiUrl: "https://api.example.com", timeout: 5000 }; | 与 Partial 相反;用于确保配置项完整 |
Readonly<T> | 将 T 的所有属性变为只读 | interface Point { x: number; y: number; } let p: Readonly<Point> = { x: 10, y: 20 }; // p.x = 30; // ❌ 错误 | 创建不可变对象;适合用于状态管理、常量数据 |
Pick<T, K> | 从 T 中选出一组属性 K,构成新类型 | interface Todo { id: number; title: string; description: string; completed: boolean; } type TodoPreview = Pick<Todo, "id" | "title">; const todo: TodoPreview = { id: 1, title: "Learn TS" }; // OK | K 必须是 keyof T 的子集;用于提取关注字段 |
Omit<T, K> | 从 T 中排除一组属性 K | type TodoInfo = Omit<Todo, "completed">; // 包含 id, title, description const info: TodoInfo = { id: 1, title: "Learn TS", description: "Master TypeScript generics" }; | 与 Pick 互补;常用于 DTO、表单数据转换 |
Record<K, T> | 构建一个对象类型,其键属于 K,值类型为 T | type Pages = "home" | "about" | "contact"; const nav: Record<Pages, string> = { home: "/", about: "/about", contact: "/contact" }; | 用于动态键的对象;替代 { [key: string]: T } 更精确 |
ReturnType<T> | 获取函数类型的返回值类型 | function getUser() { return { name: "Alice", age: 28 }; } type User = ReturnType<typeof getUser>; // { name: string; age: number } | 结合 typeof 使用;用于类型推导,避免重复定义 |
自定义工具类型
// 将所有属性转为字符串类型
type Stringify<T> = {
[P in keyof T]: string;
};
interface User { id: number; name: string; }
type StringUser = Stringify<User>;
// { id: string; name: string; }
总结建议:
| 最佳实践 | 说明 |
|---|
✅ 优先使用泛型替代 any | 保持类型安全 |
| ✅ 使用泛型约束提升类型精度 | 避免运行时错误 |
✅ 善用 keyof 和索引类型实现安全访问 | 如 getProperty 模式 |
| ✅ 内置工具类型大幅简化类型操作 | 推荐熟练掌握 Partial、Pick、Omit 等 |
| ✅ 泛型默认值提升 API 友好性 | 减少使用者负担 |
| ✅ 泛型类适合构建通用容器或组件 | 如 Box<T>、Store<T> |
6. 高级类型技巧
条件类型
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
基本语法 T extends U ? X : Y | 根据类型关系选择不同的类型分支 | type IsString<T> = T extends string ? true : false; type A = IsString<"hello">; // true type B = IsString<42>; // false | 类似三元运算符,但作用于类型层面;在编译时计算,不影响运行时 |
| 分布式条件类型 | 当 T 是联合类型时,条件类型会自动”分发”到每个成员 | type ToArray<T> = T extends any ? T[] : never; type Result = ToArray<string | number>; // string[] | number[] // 等价于: (string extends any ? string[] : never) | (number extends any ? number[] : never) | 只对”裸类型参数”(naked type parameter)生效;用于构建灵活的类型转换工具 |
实际应用:Exclude、Extract | 内置工具类型基于条件类型实现 | // Exclude<T, U>: 从 T 中排除可分配给 U 的类型 type WithoutString = Exclude<string | number | boolean, string>; // number | boolean // Extract<T, U>: 从 T 中提取可分配给 U 的类型 type OnlyString = Extract<string | number, string>; // string | Exclude<T, U> = T extends U ? never : T;Extract<T, U> = T extends U ? T : never |
映射类型
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 基本映射类型 | 基于已有类型,通过遍历其属性构建新类型 | interface Todo { title: string; completed: boolean; } type ReadOnlyTodo = { [K in keyof Todo]: Todo[K]; }; // 等价于 { title: string; completed: boolean; } // 但可添加修饰符 type Readonly<T> = { readonly [K in keyof T]: T[K]; }; type LockedTodo = Readonly<Todo>; // 所有属性只读 | keyof T 获取 T 的所有键的联合类型;[K in Keys] 遍历每个键;T[K] 获取对应属性类型 |
| 可选与只读映射 | 使用 ? 和 readonly 修饰符 | type Partial<T> = { [K in keyof T]?: T[K]; // 可选 }; type Mutable<T> = { -readonly [K in keyof T]: T[K]; // 移除只读 }; type Required<T> = { [K in keyof T]-?: T[K]; // 移除可选 }; | -? 表示移除可选性;-readonly 表示移除只读性;TypeScript 内置了这些工具类型 |
映射修饰符 as(重映射键) | 使用 as 动态修改生成的属性名(TS 4.1+) | type EventNames<T> = { [K in keyof T as \on${Capitalize<string & K>}Change`]: (v: T[K]) => void; }; interface Form { name: string; age: number; } type FormEvents = EventNames | as 后可使用模板字面量类型;需将 K 转为 string & K 才能使用 Capitalize;强大但复杂,适合高级场景 |
类型守卫(Type Guard)
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
typeof 守卫 | 用于原始类型(string、number 等)的类型收窄 | function padLeft(value: string | number, padding: string | number) { if (typeof padding === "number") { return " ".repeat(padding) + value; // padding: number } if (typeof value === "string") { return padding + value; // value: string } return padding.toString() + value.toString(); } | 仅适用于 string、number、boolean、symbol;不能用于对象、数组、null;类似 Python 的 type(obj) == Class,仅能用于基础类型 |
instanceof 守卫 | 用于类或构造函数的类型收窄 | class Dog { bark() { console.log("Woof!"); } } class Cat { meow() { console.log("Meow!"); } } function makeSound(animal: Dog | Cat) { if (animal instanceof Dog) { animal.bark(); // animal: Dog } else { animal.meow(); // animal: Cat } } | 依赖原型链,适用于 class 实例;不适用于接口(无运行时值);类似 Python 的 isinstance(obj, class),可以处理继承关系 |
in 操作符守卫 | 检查对象是否具有某属性 | interface Admin { role: string; permissions: string[]; } interface User { name: string; email: string; } function printInfo(entity: Admin | User) { if ("role" in entity) { console.log(entity.role); // entity: Admin } else { console.log(entity.email); // entity: User } } | 适用于接口或对象类型的区分;比 typeof 更灵活;类似 Python 的 hasattr |
自定义类型谓词(is) | 创建可复用的类型判断函数 | function isString(value: any): value is string { return typeof value === "string"; } function processInput(input: string | number) { if (isString(input)) { return input.toUpperCase(); // input: string } return input.toFixed(2); // input: number } | 返回类型为 value is Type;函数返回 true 时,TS 推断 value 为 Type;适合复杂判断逻辑的封装 |
类型推断(infer)
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
infer 关键字 | 在条件类型中”捕获”待推断的类型 | // 提取函数返回值类型 type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type Func = () => string; type R = ReturnType<Func>; // string // 提取数组元素类型 type ElementType<T> = T extends (infer U)[] ? U : never; type Item = ElementType<number[]>; // number | infer 只能在 extends 子句中使用;infer 声明的类型变量在 true 分支中可用 |
多重 infer | 从复杂结构中提取多个类型 | type FunctionArgs<T> = T extends (...args: infer A) => any ? A : never; type Args = FunctionArgs<(name: string, age: number) => void>; // [string, number] | 可提取参数类型、this 类型等;用于构建高级类型工具 |
实际应用:Promise<value> 解包 | 常用于处理异步类型 | type Unpacked<T> = T extends Promise<infer U> ? U extends Promise<any> ? Unpacked<U> // 递归解包 : U : T; type A = Unpacked<Promise<string>>; // string type B = Unpacked<Promise<Promise<number>>>; // number | 结合递归实现深度解包;类似 awaited 类型的行为 |
扩展:实用高级类型模式
// 递归 Partial(深度可选)
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;
// 获取类的实例类型
type InstanceType<T> = T extends new (...args: any[]) => infer R ? R : never;
总结建议:
| 最佳实践 | 说明 |
|---|
| ✅ 条件类型用于”类型逻辑判断” | 如 Exclude、NonNullable 等 |
| ✅ 映射类型用于”批量转换属性” | 如 Partial、Readonly、自定义事件类型等 |
| ✅ 类型守卫是联合类型安全使用的基石 | 必须在使用前进行收窄 |
✅ infer 是高级类型的核心工具 | 用于提取、解构复杂类型 |
| ✅ 组合使用这些技巧构建强大类型系统 | 如 ReturnType<infer R> 模式非常常见 |
7. 模块与类型管理
ES 模块与 CommonJS 互操作
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| ES 模块(ESM)语法 | 使用 import / export 的现代模块标准 | // mathUtils.ts export const add = (a: number, b: number): number => a + b; export function multiply(a: number, b: number): number { return a * b; } export default function divide(a: number, b: number): number { return a / b; } // main.ts import divide, { add, multiply } from './mathUtils'; console.log(add(2, 3)); // 5 | 推荐使用现代 ESM 语法;支持命名导出和默认导出 |
| CommonJS 语法 | Node.js 传统模块系统,使用 require / module.exports | // mathUtils.cjs (或 .js) module.exports = { add: (a: number, b: number) => a + b, multiply: (a: number, b: number) => a * b, }; // main.ts const math = require('./mathUtils'); // 或使用 import import * as math from './mathUtils'; | 在 Node.js 环境中广泛使用;TypeScript 可通过配置支持混合使用 |
esModuleInterop: true | 允许更自然地从 CommonJS 模块导入默认导出 | // 假设 lodash 是 CommonJS 模块 // tsconfig.json { "compilerOptions": { "esModuleInterop": true } } import _ from 'lodash'; // OK,即使 lodash 没有 __esModule 标记 | 解决 default 导出兼容性问题;推荐在新项目中开启 |
allowSyntheticDefaultImports | 允许对没有默认导出的模块使用 import X from 'Y' | // 即使 moment 是 CommonJS 模块 import moment from 'moment'; // OK(若 allowSyntheticDefaultImports: true) | 仅影响类型检查,不修改运行时行为;需与打包工具(如 Webpack)配合使用 |
类型声明文件(.d.ts)
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
| 声明文件作用 | 为 JavaScript 文件提供类型信息,使 TS 能进行类型检查 | // jquery.d.ts declare const $: { (selector: string): any; ajax(url: string, options?: any): void; }; declare function jQuery(selector: string): any; export default jQuery; | 文件以 .d.ts 结尾;不生成 JS 代码,仅用于类型检查;可为第三方库或全局变量提供类型 |
| 为本地 JS 文件添加类型 | 让 TS 理解 .js 文件的结构 | // utils.js exports.formatDate = (date) => { /* ... */ }; // utils.d.ts declare module './utils' { export function formatDate(date: Date): string; } | 文件名需与 JS 文件对应;使用 declare module 'path' 语法 |
| 全局声明文件 | 定义全局变量或函数的类型 | // globals.d.ts declare const VERSION: string; declare function log(message: string): void; | 放入 include 或 typeRoots 中;避免污染全局命名空间,推荐使用模块化 |
@types 社区类型库
| 概念 | 作用 | 代码示例 | 注意事项 |
|---|
@types/* 包 | DefinitelyTyped 社区维护的第三方库类型定义 | npm install --save-dev @types/lodash @types/express @types/react import _ from 'lodash'; import express from 'express'; const app = express(); app.get('/', (req, res) => { res.send('Hello'); }); | 大多数流行库都有对应的 @types 包;命名规则:@types/<npm-package-name> |
| 类型自动加载 | TypeScript 自动查找 node_modules/@types 中的类型 | // 无需手动引入 .d.ts 文件 import * as fs from 'fs'; // 内置类型 // import express from 'express'; // @types/express 提供类型 | 优先级:项目内 .d.ts > node_modules/@types > 内置类型 |
| 自定义类型覆盖 | 项目内类型优先于 @types | // src/types/express/index.d.ts declare module 'express' { interface Request { user?: { id: number; name: string }; } } | 用于扩展第三方库类型(如添加 req.user);文件结构需匹配模块路径 |
三斜线指令
⚠️ 三斜线指令是旧版 TypeScript 特性,现代项目推荐使用模块导入。仅在特定场景下使用。
| 指令 | 作用 | 代码示例 | 注意事项 |
|---|
/// <reference path="..." /> | 引入本地 .d.ts 文件 | // utils.d.ts declare function formatCurrency(amount: number): string; // main.ts /// <reference path="./utils.d.ts" /> console.log(formatCurrency(100)); | 用于文件间依赖;已被 import 取代,仅用于全局声明文件 |
/// <reference types="..." /> | 引入 @types 包中的类型 | /// <reference types="jquery" /> // 等价于:import 'jquery'; // 但不导入值,仅引入类型 | 用于不导入值但需要类型的场景(如 JSX);等价于 import 类型引用 |
/// <reference lib="..." /> | 引入 TypeScript 内置类型库 | /// <reference lib="dom" /> /// <reference lib="es2021" /> | 当 lib 未在 tsconfig.json 中配置时使用;如 dom、es5、webworker 等 |
配置建议(tsconfig.json)
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"typeRoots": ["node_modules/@types", "src/types"]
},
"include": ["src/**/*"]
}
总结建议:
| 最佳实践 | 说明 |
|---|
| ✅ 优先使用 ES 模块语法 | 更现代、可 tree-shaking |
✅ 开启 esModuleInterop 和 allowSyntheticDefaultImports | 提高模块互操作性 |
✅ 为第三方 JS 库使用 @types/* | 获得开箱即用的类型支持 |
✅ 本地 JS 文件可通过 .d.ts 添加类型 | 逐步迁移 JS 项目到 TS |
✅ 扩展第三方类型时在 src/types 下创建声明文件 | 如 Express 的 Request 扩展 |
| ✅ 避免使用三斜线指令 | 优先使用 import / export 模块语法 |
8. 编译配置(tsconfig.json)
常用编译选项:target、module、outDir、strict 等
| 选项 | 作用 | 配置示例 | 注意事项 |
|---|
target | 指定编译后的 JavaScript 版本 | { "compilerOptions": { "target": "ES2020" } } | 决定生成代码的语法兼容性(如 let、const、箭头函数);推荐使用 ES2015 或更高版本;若需支持旧浏览器,可设为 ES5,但会引入更多 polyfill 和降级代码 |
module | 指定模块系统格式 | { "compilerOptions": { "module": "ESNext" } } | ESNext:支持最新 ES 模块语法(推荐用于现代项目);CommonJS:Node.js 默认模块系统;需与运行环境或打包工具(如 Webpack、Vite)匹配 |
outDir | 指定编译后文件的输出目录 | { "compilerOptions": { "outDir": "./dist" } } | 避免源码与编译后文件混杂;建议设置为 dist、build 等独立目录;配合 .gitignore 忽略输出目录 |
rootDir | 指定源码根目录 | { "compilerOptions": { "rootDir": "./src", "outDir": "./dist" } } | 控制哪些文件参与编译;与 outDir 配合保持目录结构一致 |
strict | 启用所有严格类型检查选项的总开关 | { "compilerOptions": { "strict": true } } | 等价于启用 strictNullChecks、strictFunctionTypes、strictBindCallApply、strictPropertyInitialization、noImplicitAny、noImplicitThis、alwaysStrict;强烈建议在新项目中开启 |
strictNullChecks | 禁止 null 和 undefined 赋值给非 any 类型 | let name: string = null; // ❌ 错误(若 strictNullChecks: true) let optionalName: string | null = null; // OK | 防止空值错误;需显式声明联合类型 T | null | undefined |
noImplicitAny | 禁止隐式 any 类型推断 | function log(value) { // ❌ 参数隐式为 any console.log(value); } | 强制显式标注类型,避免类型失控;提高代码可维护性 |
esModuleInterop | 改善 ES 模块与 CommonJS 的互操作性 | { "compilerOptions": { "esModuleInterop": true } } | 允许 import _ from 'lodash' 正确导入 CommonJS 模块;推荐开启 |
allowSyntheticDefaultImports | 允许对无默认导出的模块使用默认导入 | import React from 'react'; // 即使 react 是 CommonJS | 仅影响类型检查,不改变运行时;通常与 esModuleInterop 一起使用 |
types / typeRoots | 控制类型声明文件的查找范围 | { "compilerOptions": { "types": ["node", "jest"], "typeRoots": ["./typings", "node_modules/@types"] } } | types:白名单,只包含指定包的类型;typeRoots:自定义类型声明目录优先级 |
增量编译与构建优化
| 概念 | 作用 | 配置/示例 | 注意事项 |
|---|
| 增量编译(Incremental) | 只重新编译修改过的文件及其依赖,大幅提升构建速度 | { "compilerOptions": { "incremental": true, "composite": true } } | 首次编译仍较慢,后续编译显著加快;TS 会生成 .tsbuildinfo 文件记录编译状态;适合大型项目 |
tsbuildinfo 文件 | 存储上次编译的元信息(如时间戳、依赖图) | 自动生成于 outDir 或指定路径 | 可提交到版本控制(避免 CI 重复全量编译);删除后下次编译变为全量 |
构建模式(--build) | 使用 tsc --build 支持项目引用和增量构建 | tsc --build # 构建全部 tsc --build --clean # 清理构建状态 tsc --build --watch # 监听并增量编译 | 替代 tsc 命令用于复杂项目;支持多项目配置(Project References) |
| 项目引用(Project References) | 将大项目拆分为多个子项目,实现按需编译 | // tsconfig.base.json { "compilerOptions": { /* 公共配置 */ } } // packages/utils/tsconfig.json { "extends": "../tsconfig.base.json", "references": [ { "path": "../shared" } ] } | 实现逻辑分离与独立构建;修改一个包只触发相关联的增量编译;适合 monorepo 架构 |
declaration: true | 生成 .d.ts 类型声明文件 | { "compilerOptions": { "declaration": true, "declarationMap": true } } | 发布 npm 包时必须开启,提供类型支持;生成的 .d.ts 文件可被其他 TS 项目使用 |
skipLibCheck: true | 跳过对 .d.ts 文件的类型检查 | { "compilerOptions": { "skipLibCheck": true } } | 加快编译速度,尤其当使用大量 @types 包时;安全:因第三方类型通常已验证;仅跳过库文件,不影响业务代码检查 |
推荐基础 tsconfig.json 示例
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"incremental": true,
"declaration": true,
"declarationMap": true
},
"include": ["src"]
}
总结建议:
| 最佳实践 | 说明 |
|---|
✅ 新项目务必开启 "strict": true | 最大程度利用类型系统优势 |
✅ 设置合理的 outDir 和 rootDir | 保持项目结构清晰 |
✅ 开启 incremental: true | 显著提升大型项目构建效率 |
✅ 发布库时启用 declaration: true | 提供完整的类型支持 |
✅ 使用 tsc --build 管理复杂项目 | 支持增量、清理、监听等高级功能 |
✅ 合理使用 skipLibCheck 加速编译 | 不影响业务代码安全性 |
✅ 配置 .gitignore 忽略 dist/、.tsbuildinfo | 避免提交编译产物(除非必要) |
9. 工程化与工具链
与 ESLint、Prettier 集成
| 工具 | 作用 | 配置示例 | 注意事项 |
|---|
| ESLint(代码质量检查) | 静态分析代码,检测潜在错误、风格问题和类型安全漏洞 | npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin // .eslintrc.json { "root": true, "parser": "@typescript-eslint/parser", "plugins": ["@typescript-eslint"], "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", "plugin:@typescript-eslint/recommended-requiring-type-checking" ], "parserOptions": { "project": "./tsconfig.json" }, "rules": { "@typescript-eslint/no-unused-vars": "error" } } | @typescript-eslint/parser:让 ESLint 能解析 TS 语法;@typescript-eslint/eslint-plugin:提供 TS 特有规则;project 选项启用类型检查规则;推荐使用 eslint --ext .ts,.tsx src 检查 |
| Prettier(代码格式化) | 统一代码风格,自动格式化(缩进、引号、换行等) | npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier // .prettierrc { "semi": true, "singleQuote": true, "arrowParens": "avoid", "trailingComma": "es5" } // .eslintrc.json(续) { "extends": [ "...", "plugin:prettier/recommended" ] } | eslint-config-prettier:禁用与 Prettier 冲突的 ESLint 规则;eslint-plugin-prettier:将 Prettier 作为 ESLint 规则运行;推荐在编辑器中启用”保存时自动格式化” |
| VS Code 集成 | 实现编辑器内实时 lint 和格式化 | // .vscode/settings.json { "editor.formatOnSave": true, "editor.codeActionsOnSave": { "source.fixAll.eslint": true }, "eslint.validate": ["typescript", "typescriptreact"] } | 安装 VS Code 插件:ESLint、Prettier - Code formatter;确保项目根目录有 .eslintrc 和 .prettierrc |
与构建工具(Webpack、Vite)配合
| 构建工具 | 集成方式 | 配置要点 | 注意事项 |
|---|
| Webpack | 使用 ts-loader 或 babel-loader 编译 TypeScript | npm install --save-dev ts-loader // webpack.config.js module.exports = { entry: './src/index.ts', module: { rules: [ { test: /\.tsx?$/, use: 'ts-loader', exclude: /node_modules/, }, ], }, resolve: { extensions: ['.tsx', '.ts', '.js'], }, output: { filename: 'bundle.js', path: path.resolve(__dirname, 'dist'), }, }; | ts-loader:直接调用 tsc 编译,支持类型检查;babel-loader + @babel/preset-typescript:仅做语法转换,不进行类型检查,需配合 fork-ts-checker-webpack-plugin;extensions 需包含 .ts、.tsx |
| Vite | 原生支持 TypeScript,零配置启动 | // src/main.ts import { createApp } from 'vue'; import App from './App.vue'; createApp(App).mount('#app'); // vite.config.ts import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], }); | Vite 使用 esbuild 预构建 TS,极快;开发阶段不进行类型检查,需单独运行 tsc --noEmit 或使用 vite-plugin-checker;生产构建使用 Rollup |
| 类型检查插件 | 在构建过程中加入类型检查 | # Webpack npm install --save-dev fork-ts-checker-webpack-plugin # Vite npm install --save-dev vite-plugin-checker // vite.config.ts import checker from 'vite-plugin-checker'; export default defineConfig({ plugins: [ vue(), checker({ typescript: true }) ], }); | 避免”类型错误但构建成功”的问题;推荐在 CI/CD 中运行 tsc --noEmit 作为最终检查 |
开发热重载(ts-node、tsx)
| 工具 | 作用 | 使用方式 | 注意事项 |
|---|
ts-node | 直接运行 .ts 文件,无需预编译 | npm install --save-dev ts-node # 运行 TypeScript 文件 npx ts-node src/index.ts # 结合 nodemon 实现热重载 npx nodemon --exec "ts-node" src/index.ts | 适合后端 Node.js 服务开发;首次启动较慢(需编译);不适用于前端浏览器环境 |
tsx(推荐) | 基于 esbuild 的极速 TS/TSX 运行器,支持 ESM 和热重载 | npm install --global tsx # 运行脚本 npx tsx src/index.ts # 支持 JSX 和 ESM npx tsx src/app.tsx # 监听文件变化(热重载) npx tsx watch src/index.ts | 比 ts-node 快 10-100 倍;原生支持 import、export、JSX;内置 watch 模式,无需 nodemon;推荐用于现代 TS 项目开发 |
vite-node | Vite 生态的运行时,支持 Vite 插件和 HMR | npm install --save-dev vite-node # 通常用于测试框架(如 Vitest) npx vitest | 与 Vite 配置一致,适合全栈项目;主要用于测试环境 |
推荐开发脚本(package.json)
{
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"lint": "eslint src --ext .ts,.tsx",
"format": "prettier --write src",
"type-check": "tsc --noEmit",
"preview": "node dist/index.js"
}
}
工程化最佳实践总结:
| 实践 | 说明 |
|---|
| ✅ 统一代码风格 | ESLint + Prettier + Editor Integration,确保团队一致性 |
| ✅ 构建工具选择 | 前端项目:Vite(开发快,配置少);复杂项目:Webpack(灵活性高) |
| ✅ 开发体验优化 | 使用 tsx 或 ts-node + nodemon 实现快速启动与热重载 |
| ✅ 类型检查不遗漏 | 即使使用 Vite 或 babel,也需在 CI 中运行 tsc --noEmit |
| ✅ 配置文件共用 | 将 ESLint、Prettier 配置提取为独立包(如 @myorg/eslint-config),便于多项目复用 |
| ✅ 性能监控 | 使用 --diagnostics 或 --extendedDiagnostics 查看 tsc 性能瓶颈 |
10. 最佳实践
避免滥用 any
| 原则 | 说明 | 错误示例 | 正确做法 | 注意事项 |
|---|
any 是类型安全的”后门” | 使用 any 会关闭类型检查,失去 TypeScript 的核心优势 | function processData(data: any) { data.randomMethod(); // ❌ 运行时可能报错 return data.value * 2; // ❌ 可能 NaN } // 调用 processData({ name: "Alice" }); // 无编译错误,但运行失败 | interface Data { value: number; } function processData(data: Data) { return data.value * 2; // 类型安全 } // processData({ name: "Alice" }); // ❌ 编译时报错 | 仅在临时调试或无法避免的第三方库交互中使用;优先使用更精确的类型(如 unknown、泛型、接口) |
| 替代方案:使用泛型 | 保持类型灵活性的同时保证安全 | function identity<T>(arg: T): T { return arg; } const result = identity({ name: "Bob" }); // 类型为 { name: string } | - | 适用于函数、组件等通用逻辑;避免”类型擦除” |
| 替代方案:联合类型 + 类型守卫 | 处理多种可能类型 | function handleInput(input: string | number) { if (typeof input === "string") { return input.toUpperCase(); } return input.toFixed(2); } | - | 结合 typeof、instanceof、in 安全收窄类型 |
合理使用 unknown
| 原则 | 说明 | 代码示例 | 注意事项 |
|---|
unknown 是 any 的安全替代 | unknown 表示”未知类型”,不能直接操作,必须先进行类型检查 | function safeParse(json: string): unknown { return JSON.parse(json); } const data = safeParse('{ "name": "Alice" }'); // ❌ 错误:不能直接访问 // console.log(data.name); // 必须先类型守卫 if (typeof data === "object" && data !== null && "name" in data) { console.log((data as { name: string }).name); } // 或使用类型谓词 function isUser(obj: any): obj is { name: string } { return !!obj && typeof obj === "object" && "name" in obj; } if (isUser(data)) { console.log(data.name); // OK,类型收窄 } | 适用于:API 响应、JSON 解析、用户输入等不可信数据;比 any 安全,强制类型验证;在函数返回未知结构数据时,优先返回 unknown;接收第三方数据时,参数类型设为 unknown |
any vs unknown 对比:
any:可随意读写、调用方法(失去类型安全)
unknown:必须通过类型守卫才能使用
类型驱动开发(TDD)
| 原则 | 说明 | 开发流程示例 | 注意事项 |
|---|
| 先定义类型,再实现逻辑 | 通过类型系统设计 API 接口,驱动代码实现 | // Step 1: 定义类型 interface User { id: number; name: string; email: string; } type ApiResponse<T> = { success: true; data: T; } | { success: false; error: string; }; function fetchUser(id: number): Promise<ApiResponse<User>> { // Step 2: 实现逻辑(类型已知) return fetch(\/api/users/${id}`) .then(res => res.json()) .then(data => { if (data.success) { return { success: true, data: data.data as User }; } return { success: false, error: data.error }; }); } // Step 3: 使用(类型安全) fetchUser(1).then(result => { if (result.success) { console.log(result.data.name); // 自动提示 } else { console.error(result.error); } });` | 类型即文档,提升可维护性;减少运行时错误;适合设计 API、状态管理、组件 props |
| 工具支持 | 编辑器基于类型提供智能提示、自动补全、重构支持 | 使用 VS Code 可实时看到类型推断;类型错误在编码阶段暴露 | - |
渐进式迁移 JS 项目到 TS
| 阶段 | 策略 | 操作步骤 | 注意事项 |
|---|
| 1. 初始化配置 | 引入 TypeScript,但不强制检查 | npm install --save-dev typescript // tsconfig.json { "compilerOptions": { "target": "ES2020", "module": "ESNext", "allowJs": true, "checkJs": false, "outDir": "./dist", "rootDir": "./src" }, "include": ["src"] } | allowJs: true:允许 .js 文件存在;checkJs: false:暂不检查 JS 文件类型 |
| 2. 重命名与增量添加类型 | 将部分 .js 文件改为 .ts,逐步添加类型 | mv src/utils.js src/utils.ts // src/utils.ts export function formatDate(date: Date): string { return date.toLocaleDateString(); } | 优先迁移工具函数、类型稳定模块;使用 // @ts-ignore 临时忽略错误(需标注原因);仍可混合使用 JS 和 TS |
| 3. 启用严格检查 | 分阶段开启严格模式 | { "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true } } | 不要一次性开启所有 strict 选项;使用 tsc --noEmit --watch 实时查看错误 |
| 4. 补充类型声明 | 为第三方库或全局变量添加 .d.ts | // src/types/global.d.ts declare const VERSION: string; // src/types/lodash.d.ts declare module 'lodash' { export function debounce<T extends (...args: any[]) => any>(func: T, wait?: number): T; } | 使用 @types/* 优先;自定义声明避免过度泛化 |
| 5. 全量迁移与维护 | 所有文件使用 .ts,持续维护类型质量 | // 最终 tsconfig.json { "compilerOptions": { "strict": true, "allowJs": false } } | 建立 CI 流程:tsc --noEmit 作为构建步骤;团队培训:避免新增 any |
推荐 ESLint 规则防止 any 滥用
{
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/no-unsafe-argument": "error",
"@typescript-eslint/no-unsafe-assignment": "error",
"@typescript-eslint/no-unsafe-call": "error",
"@typescript-eslint/no-unsafe-member-access": "error"
}
最佳实践总结表:
| 实践 | 推荐做法 | 避免做法 |
|---|
| 类型安全 | 使用 unknown + 类型守卫 | 直接使用 any |
| 函数参数 | 明确接口或泛型 | any 或 Object |
| 第三方数据 | unknown → 类型验证 → 类型断言 | 直接 as MyType |
| 项目迁移 | 渐进式:.js → .ts → 添加类型 → 严格模式 | 一次性重写全部代码 |
| 团队协作 | 统一 ESLint + Prettier + no-any 规则 | 放任 any 泛滥 |
11. TypeScript 与 JavaScript 编程习惯对比表
| 操作名称 | JavaScript 实现 | TypeScript 实现 | 两者区别说明 |
|---|
| 变量声明(带类型) | let age = 25; const name = "Alice"; | let age: number = 25; const name: string = "Alice"; | TS 显式声明类型,JS 依赖运行时推断。TS 可防止类型错误(如 age = "30" 会报错) |
| 函数参数与返回值 | function add(a, b) { return a + b; } | function add(a: number, b: number): number { return a + b; } | TS 要求参数和返回值类型明确,调用时传错类型会编译报错。JS 只在运行时暴露问题 |
| 对象结构定义 | const user = { name: "Bob", age: 30 }; | interface User { name: string; age: number; } const user: User = { name: "Bob", age: 30 }; | TS 使用 interface 或 type 定义结构,确保对象符合预期,避免拼写错误或缺失字段 |
| 数组类型定义 | const list = [1, 2, 3]; | const list: number[] = [1, 2, 3]; // 或 const list: Array<number> = [1, 2, 3]; | TS 明确数组元素类型,防止插入非法类型(如 list.push("4") 会报错) |
| 可选属性 | function logUser(user) { console.log(user.name); if (user.email) { console.log(user.email); } } | interface User { name: string; email?: string; } function logUser(user: User) { console.log(user.name); if (user.email) { console.log(user.email); } } | TS 使用 ? 明确表示属性可选,调用时必须检查是否存在,提升代码健壮性 |
| 联合类型 | function formatStatus(status) { return status === 1 ? "Active" : "Inactive"; } | type Status = 1 | 0; function formatStatus(status: Status): string { return status === 1 ? "Active" : "Inactive"; } | TS 使用 1 | 0 限制值范围,传入 2 会报错。JS 无法限制 |
| 枚举类型 | const STATUS_ACTIVE = 1; const STATUS_INACTIVE = 0; if (user.status === STATUS_ACTIVE) { ... } | enum Status { Active = 1, Inactive = 0 } if (user.status === Status.Active) { ... } | TS 枚举提供命名空间和类型安全,避免魔法数字。JS 需手动定义常量 |
| 函数重载(模拟) | function handleInput(value) { if (typeof value === "string") { return value.toUpperCase(); } else { return value * 2; } } | function handleInput(value: string): string; function handleInput(value: number): number; function handleInput(value: string | number): string | number { if (typeof value === "string") { return value.toUpperCase(); } else { return value * 2; } } | TS 支持函数重载签名,提供更精确的类型提示。JS 仅靠运行时判断 |
| 类与访问修饰符 | class Person { constructor(name) { this.name = name; } } | class Person { private name: string; public age: number; constructor(name: string, age: number) { this.name = name; this.age = age; } } | TS 支持 private、public、protected,控制属性访问权限,增强封装性 |
接口实现(implements) | 无原生支持 | interface Drawable { draw(): void; } class Circle implements Drawable { draw() { console.log("Drawing a circle"); } } | TS 支持类实现接口,强制类包含指定方法,提升代码规范性和可扩展性 |
| 泛型使用 | function identity(value) { return value; } | function identity<T>(value: T): T { return value; } const result = identity<string>("hello"); | TS 泛型保持类型信息,调用时能推断返回类型。JS 无法保留类型 |
| 类型断言(强制转型) | 无对应机制 | const input = document.getElementById("name") as HTMLInputElement; input.value = "Hello"; | TS 允许开发者”断言”某个值的类型,绕过类型检查(需谨慎使用) |
| 空值检查 | if (user && user.profile) { ... } | if (user?.profile) { ... } // 可选链 // 或使用非空断言(需确保安全) user!.profile | TS 支持可选链 ?. 和非空断言 !,语法更简洁,但 ! 有风险 |
| 模块导入 | import React from 'react'; import { useState } from 'react'; | import React from 'react'; import { useState } from 'react'; // 类型导入(可省略) import type { User } from './types'; | 导入语法一致,但 TS 支持 import type 仅导入类型,不参与运行时 |
| 基本箭头函数 | const add = (a, b) => { return a + b; }; | const add = (a: number, b: number): number => { return a + b; }; | TS 明确参数和返回值类型,防止传入字符串等非法类型。JS 无类型约束 |
| 单表达式隐式返回 | const square = x => x * x; | const square = (x: number): number => x * x; | TS 仍需标注参数类型,返回类型可自动推断。JS 无类型信息 |
| 无参数箭头函数 | const greet = () => { console.log("Hello"); }; | const greet = (): void => { console.log("Hello"); }; | TS 可显式声明返回类型为 void(无返回值),增强语义清晰度 |
| 返回对象字面量 | const createUser = (name, age) => ({ name: name, age: age }); | interface User { name: string; age: number; } const createUser = (name: string, age: number): User => ({ name, age }); | TS 使用接口定义返回对象结构,确保字段正确。JS 无法验证对象形状 |
| 回调函数(数组方法) | const numbers = [1, 2, 3]; const doubled = numbers.map(n => n * 2); | const numbers: number[] = [1, 2, 3]; const doubled: number[] = numbers.map((n: number): number => n * 2); | TS 可推断类型,但显式标注提升可读性。TS 能防止传错类型到 map |
| 事件处理函数 | const button = document.getElementById("btn"); button.addEventListener("click", (e) => { console.log(e.target); }); | const button = document.getElementById("btn"); if (button) { button.addEventListener("click", (e: MouseEvent) => { console.log((e.target as HTMLElement).innerText); }); } | TS 需断言 e.target 类型,且事件对象类型更精确(如 MouseEvent) |
| 异步箭头函数 | const fetchData = async (url) => { const res = await fetch(url); return await res.json(); }; | interface ApiResponse { data: any; } const fetchData = async (url: string): Promise<ApiResponse> => { const res = await fetch(url); return await res.json(); }; | TS 使用 Promise<T> 明确异步函数返回的是 Promise 且解析后类型为 T |
| 高阶函数返回箭头函数 | const createMultiplier = (factor) => { return (num) => num * factor; }; | const createMultiplier = (factor: number): ((num: number) => number) => { return (num: number): number => num * factor; }; | TS 显式声明返回的是一个函数类型,提升类型安全性与可读性 |