Skip to content

TypeScript 系统学习文档 ​

  1. 优先类型推断,必要时显式注解
  2. strict: true 始终开启
  3. 公共 API 用 interface,复杂类型表达式用 type
  4. 用 unknown 替代 any
  5. 用可辨识联合管理状态
  6. 用泛型编写可复用的类型安全组件
  7. 函数参数必须注解,返回值尽量让 TS 推断
  8. 纯数据结构用 interface/type,有状态行为的用 class
  9. 枚举优先用字符串枚举,或考虑用联合类型替代
  10. 类型断言(as)是逃生舱,不是常规手段
  11. 用 satisfies 替代类型注解来保留精确推断(TS 4.9+)
  12. 用 NoInfer 防止泛型从非意图位置推断(TS 5.4+)
  13. filter(x => x !== undefined) 在 TS 5.5+ 中自动推断类型
  14. 使用 isolatedDeclarations 确保声明文件与源码一致性(TS 5.5+)

本文档根据 TypeScript 官方文档(https://www.typescriptlang.org/)系统整理,涵盖从基础到进阶的核心知识点。 每个知识点均包含定义、注意事项、以及相似概念对比,适合系统学习 TypeScript 的开发者。

目录 ​

  1. TypeScript 简介
  2. 环境搭建与第一个程序
  3. 基础类型
  4. 类型注解
  5. 接口(Interfaces)
  6. 类型别名(Type Aliases)
  7. 联合类型与交叉类型
  8. 数组与元组
  9. 函数
  10. 对象类型
  11. 泛型(Generics)
  12. 枚举(Enums)
  13. 类(Classes)
  14. 类型缩小(Narrowing)
  15. 类型操作
  16. 工具类型(Utility Types)
  17. 模块系统
  18. 声明文件
  19. 装饰器(Decorators)
  20. TypeScript 配置
  21. 实际项目最佳实践
  22. TypeScript 常见陷阱与反模式
  23. TypeScript 版本演进与核心新特性
  24. 高级类型特殊写法与类型体操

1. TypeScript 简介 ​

定义 ​

TypeScript 是 JavaScript 的超集(Superset),由微软开发并维护。它在 JavaScript 的基础上增加了静态类型系统(Static Type System),让你在编码阶段就能发现类型相关的错误,而不是等到运行时。

核心特点:

  • 所有合法的 JavaScript 代码都是合法的 TypeScript 代码
  • TypeScript 最终被编译为 JavaScript 执行
  • 类型系统只在编译期生效,运行时无额外开销

为什么选择 TypeScript ​

  • 更早发现错误:编译器在开发阶段就能捕获类型错误
  • 更好的开发体验:IDE 自动补全、重构、导航更强大
  • 自文档化:类型本身就是文档
  • 渐进式采用:可以在现有 JS 项目中逐步引入
  • 生态成熟:主流框架(React、Vue、Angular)都支持 TypeScript

注意事项 ​

TypeScript 不改变 JavaScript 的运行时行为。类型检查只在编译期进行,编译后的 JS 文件中所有类型信息都被擦除了。这意味着 TypeScript 不能防止运行时错误(如网络请求失败、用户非法输入等),它只能帮你减少类型相关的逻辑错误。

学习曲线:如果你对 JavaScript 还不够熟悉,建议先掌握 JS 的核心概念再学习 TypeScript,否则类型系统的复杂性反而会成为负担。

不要过度设计类型:新手容易走入"把所有东西都用类型精确描述"的误区。在实际项目中,合理使用类型推断、必要时才显式标注类型,才能达到最好的开发效率。

实际场景 ​

任何中大型前端项目都应该使用 TypeScript。比如开发一个电商后台管理系统,TypeScript 能帮助你定义清晰的数据模型,避免因接口字段拼写错误导致的线上 bug。


2. 环境搭建与第一个程序 ​

定义 ​

TypeScript 环境搭建指的是在本地开发环境中安装 TypeScript 编译器及相关工具,使得 .ts 文件能够被编译为 .js 文件执行。

安装 ​

bash
# 全局安装 TypeScript
npm install -g typescript

# 查看版本
tsc --version

# 在项目中安装(推荐,避免版本冲突)
npm install --save-dev typescript

初始化项目 ​

bash
# 创建项目目录
mkdir my-ts-project
cd my-ts-project

# 初始化 tsconfig.json
tsc --init

第一个 TypeScript 程序 ​

typescript
// hello.ts
function greet(name: string): string {
  return `Hello, ${name}!`;
}

const message = greet('TypeScript');
console.log(message); // 输出: Hello, TypeScript!

编译与运行 ​

bash
# 编译 TypeScript 文件为 JavaScript
tsc hello.ts

# 编译后生成 hello.js,然后用 Node 运行
node hello.js

# 或使用 ts-node 直接运行
npx ts-node hello.ts

注意事项 ​

全局安装 vs 项目安装:全局安装的 tsc 版本可能与项目依赖的 TypeScript 版本不一致,导致编译行为差异。团队协作中,建议每个项目都本地安装 TypeScript,并通过 npx tsc 调用,确保所有人使用相同版本。

tsc --init 会生成带有大量注释的 tsconfig.json,其中大部分选项是注释掉的。建议只保留你需要的配置项,保持配置文件简洁。

实际场景 ​

搭建新项目时,第一件事就是用 tsc --init 生成配置,然后根据项目需求调整 tsconfig.json。如果有现成的模板(如 create-react-app --template typescript),直接用模板创建就好。


3. 基础类型 ​

定义 ​

基础类型(Primitive Types)是 TypeScript 类型系统中最基本的构建单元,对应 JavaScript 中的原始数据类型。

3.1 布尔值(boolean) ​

定义:表示逻辑上的真(true)或假(false)。

typescript
let isDone: boolean = false;
let isLoggedIn: boolean = true;

// 实际场景:表示用户登录状态、开关状态
const isAdmin: boolean = user.role === 'admin';

注意事项:boolean 是小写,Boolean 是 JavaScript 的包装对象类型。在 TypeScript 中始终使用小写的 boolean。

3.2 数字(number) ​

定义:表示所有数值,包括整数和浮点数。TypeScript 中的 number 是 64 位双精度浮点数,与 JavaScript 一致。

typescript
let count: number = 42;
let price: number = 19.99;
let hex: number = 0xf00d;   // 十六进制
let binary: number = 0b1010; // 二进制
let octal: number = 0o744;   // 八进制

// 实际场景:价格计算、数量统计、页面分页
const pageSize: number = 20;
const totalPages: number = Math.ceil(total / pageSize);

注意事项:TypeScript 没有 int 或 float 的子类型,所有数值都是 number。大整数(BigInt)使用 bigint 类型。

3.3 字符串(string) ​

定义:表示文本数据,可以用单引号、双引号或模板字符串(反引号)表示。

typescript
let name: string = 'Alice';
let greeting: string = `Hello, ${name}!`; // 模板字符串

// 实际场景:用户输入处理、API URL 拼接
const apiUrl: string = `https://api.example.com/users/${userId}`;

3.4 null 与 undefined ​

定义:

  • null:表示"有意的空值",通常用于显式地表示"没有值"
  • undefined:表示"未定义",变量声明但未赋值时的默认值
typescript
let u: undefined = undefined;
let n: null = null;

// 实际场景:API 响应中可能不存在的字段
type ApiResponse = {
  data: string | null;
  error?: string; // 可选属性,可能为 undefined
};

注意事项:

null 和 undefined 的行为受 strictNullChecks 配置影响:

  • 开启 strictNullChecks(推荐)时,null 和 undefined 只能赋值给它们自身和 void 类型,除非你明确使用联合类型(如 string | null)。
  • 关闭 strictNullChecks 时,null 和 undefined 可以赋值给任何类型,这会导致大量潜在的运行时错误。

3.5 void、never 与 unknown ​

void ​

定义:表示函数没有返回值。一个不返回任何值的函数,其返回类型就是 void。

typescript
// void - 函数没有返回值
function logMessage(msg: string): void {
  console.log(msg);
}

never ​

定义:表示"永远不会有值"的类型。函数抛出异常或进入无限循环时,返回值类型为 never。

typescript
// never - 函数永远不会正常返回
function throwError(message: string): never {
  throw new Error(message);
}

function infiniteLoop(): never {
  while (true) {}
}

unknown ​

定义:类型安全的 any。表示一个"不知道具体类型"的值,在使用前必须进行类型检查。

typescript
// unknown - 类型安全的 any
let userInput: unknown = 'hello';
// unknown 需要类型检查后才能使用
if (typeof userInput === 'string') {
  console.log(userInput.toUpperCase());
}

// 实际场景:
// void -> 事件处理函数、日志函数
// never -> 错误处理、穷尽性检查
// unknown -> 解析 JSON、接收第三方 API 数据

对比:any vs unknown vs never vs void ​

特征anyunknownnevervoid
定义放弃类型检查类型安全的 any永远不会有值没有返回值
可赋值给任何类型是否(需要类型检查)是否
可调用方法是(不检查)否(需要先检查类型)不适用不适用
使用场景快速原型、迁移旧 JS接收不确定的外部数据异常函数、穷尽性检查无返回值的函数
推荐使用❌ 尽量避免✅ 替代 any✅ 精确表达✅ 常规使用

相同点:都是 TypeScript 中"特殊"的类型,不对应 JavaScript 的运行时值。

不同点:

  • any 完全跳过类型检查,是类型系统的"逃生舱",应尽量避免
  • unknown 强制你在使用前做类型检查,比 any 安全得多
  • never 表示逻辑上不可能的值,常用于穷尽性检查
  • void 表示函数没有返回值,是最常见的类型之一

关键注意事项:

void 类型的变量可以赋值为 undefined(在开启 strictNullChecks 时),但不能赋值为 null。而一个 void 返回类型的函数,实际返回的值是 undefined。

never 在联合类型中有"吸收"效果:string | never 的结果是 string,因为 never 在联合中会被自动忽略。

不要用 any 来逃避类型问题。如果确实不知道类型,先用 unknown,然后用类型守卫逐步缩小类型范围。


4. 类型注解 ​

定义 ​

类型注解(Type Annotations)是显式地为变量、参数或返回值指定类型的语法。用冒号 : 后跟类型名称表示。

类型推断 ​

定义:TypeScript 能根据赋值的值自动推断出变量的类型,无需显式注解。

typescript
// 类型推断(推荐,代码更简洁)
let name = 'Alice'; // TypeScript 自动推断为 string

// 函数参数必须注解(TypeScript 无法从内部推断)
function greet(name: string): string {
  return `Hello, ${name}!`;
}

// 实际场景:定义 API 处理函数
async function fetchUser(id: number): Promise<User> {
  const response = await fetch(`/api/users/${id}`);
  return response.json();
}

注意事项 ​

函数返回值注解可以省略:当函数体很简单时,TypeScript 可以推断返回类型。但复杂的函数建议显式标注返回类型,既是文档也能防止实现错误。

const 声明的字面量会被推断为最具体的类型:

typescript
const name = 'Alice'; // 推断为 'Alice'(字面量类型),而不是 string
let name = 'Alice';   // 推断为 string

这是因为 const 不可重新赋值,它的值永远不会变化。

对比:类型注解 vs 类型推断 ​

特征类型注解类型推断
定义开发者显式写明类型TypeScript 自动推导类型
语法: type无需额外语法
何时使用函数参数、复杂函数返回值、需要精确控制类型时简单变量、简单函数返回值
推荐度按需使用✅ 优先使用

5. 接口(Interfaces) ​

定义 ​

接口(Interface)是 TypeScript 中定义对象结构的主要方式,它规定了对象应该有哪些属性和方法,以及它们的类型。

基础用法 ​

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

function printUser(user: User): void {
  console.log(`${user.name} (${user.email})`);
}

const alice: User = {
  id: 1,
  name: 'Alice',
  email: 'alice@example.com',
};
printUser(alice);

可选属性与只读属性 ​

typescript
interface Config {
  readonly apiKey: string; // 初始化后不可修改
  baseUrl: string;
  timeout?: number; // 可选属性
  retry?: boolean;
}

接口扩展 ​

typescript
interface Animal {
  name: string;
  age: number;
}

interface Dog extends Animal {
  breed: string;
  bark(): void;
}

注意事项 ​

接口只描述形状(Shape),不描述实现。接口不会编译为任何 JavaScript 代码——它只在编译期用于类型检查。

可选属性与 undefined 的区别:

  • timeout?: number 表示该属性可以不存在,或者值为 number
  • timeout: number | undefined 表示该属性必须存在,但值可以是 number 或 undefined 这两种语义不同,使用时要明确你的意图。

readonly 只在编译期生效。在运行时,对象的只读属性仍然可以被修改。它不会生成任何运行时保护机制。

实际场景 ​

接口最常用于定义 API 响应的数据结构:

typescript
interface ApiResponse<T> {
  code: number;
  message: string;
  data: T;
}

interface Product {
  id: number;
  name: string;
  price: number;
  stock: number;
}

async function getProduct(id: number): Promise<ApiResponse<Product>> {
  const res = await fetch(`/api/products/${id}`);
  return res.json();
}

6. 类型别名(Type Aliases) ​

定义 ​

类型别名(Type Alias)为已有的类型创建新的名字,常用于联合类型、交叉类型和复杂的对象类型。

基础用法 ​

typescript
type ID = string | number;
type Status = 'active' | 'inactive' | 'pending';

type Point = {
  x: number;
  y: number;
};

注意事项 ​

类型别名不能重复声明。与接口不同,同名的类型别名会导致编译错误:

typescript
type User = { name: string; };
type User = { age: number; }; // ❌ 错误:重复标识符

类型别名不是创建了一个新类型,它只是给现有类型起了一个新名字。type ID = string | number 中的 ID 与 string | number 是完全等价的,可以互相赋值。

对比:Interface vs Type ​

这是 TypeScript 初学者最困惑的问题之一。

对比维度InterfaceType
定义定义对象结构的声明方式为类型创建别名的声明方式
适用范围只能描述对象类型可以描述任何类型(对象、联合、交叉、元组等)
声明合并✅ 支持(同名自动合并)❌ 不支持
扩展方式extends 关键字&(交叉类型)
Mapped Types❌ 不支持✅ 支持
兼容性互为兼容,可互相赋值同为结构类型,可互相赋值
推荐使用定义公共 API、对象结构联合类型、交叉类型、复杂类型表达式

相同点:

  • 都可以描述对象类型
  • 都支持泛型
  • 都是结构类型系统——只要形状匹配就认为类型兼容

不同点:

  • interface 支持声明合并(同名自动合并),type 不支持
  • type 可以描述联合类型、交叉类型、映射类型等,interface 只能描述对象
  • interface 通过 extends 扩展,type 通过 & 扩展
  • interface 在错误信息中会显示接口名,更易读;type 可能展开为具体类型
typescript
// Interface 可重复声明(合并)
interface User { name: string; }
interface User { age: number; }
// User 现在有 name 和 age

// Type 可以表示联合类型、交叉类型
type Result = Success | Error;
type Shape = Circle & Square;

// 实际场景
type ButtonVariant = 'primary' | 'secondary' | 'ghost' | 'danger';
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

最佳实践:优先使用 interface 定义公共 API 和对象结构;使用 type 定义联合类型、交叉类型、工具类型。


7. 联合类型与交叉类型 ​

7.1 联合类型(Union Types) ​

定义 ​

联合类型用 | 分隔,表示一个值可以是几种类型中的任意一种。

typescript
type StringOrNumber = string | number;

function padLeft(value: string, padding: StringOrNumber): string {
  if (typeof padding === 'number') {
    return ' '.repeat(padding) + value;
  }
  return padding + value;
}

可辨识联合(Discriminated Unions) ​

定义:一种特殊的联合类型模式,每个成员都有一个相同的字面量属性(称为"判别式"),TypeScript 可以据此精确缩小类型。

typescript
// 可辨识联合(最实用的模式)
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number }
  | { kind: 'triangle'; base: number; height: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius ** 2;
    case 'rectangle':
      return shape.width * shape.height;
    case 'triangle':
      return (shape.base * shape.height) / 2;
  }
}

7.2 交叉类型(Intersection Types) ​

定义 ​

交叉类型用 & 连接,表示一个值同时具有所有类型的全部属性和方法。

typescript
interface Person { name: string; age: number; }
interface Employee { employeeId: number; department: string; }

type Staff = Person & Employee;
const staff: Staff = { name: 'Alice', age: 30, employeeId: 12345, department: 'Engineering' };

对比:联合类型 vs 交叉类型 ​

对比维度联合类型(|)交叉类型(&)
定义值可以是其中之一值必须兼具所有类型
逻辑类比逻辑 OR(或)逻辑 AND(与)
属性数量只能访问公共属性可以访问所有属性
使用场景可能为多种类型之一合并多个类型
与 interface 的关系只能用 type 定义可用 type 定义;接口用 extends
类型缩小需要类型守卫不需要(所有属性直接可用)

注意事项:

联合类型使用时的限制:当访问联合类型的值时,只能访问所有类型都共有的成员(公共属性)。要访问特定类型的成员,必须先通过类型守卫(typeof、instanceof、可辨识联合等)缩小类型范围。

交叉类型可能产生 never:如果两个类型的不兼容字段交叉,会得到 never。例如 { a: string } & { a: number } 的结果是 { a: never }。

接口交叉 vs 接口扩展:type Staff = Person & Employee 与 interface Staff extends Person, Employee 功能类似,但接口扩展更"正规",错误信息更友好。


8. 数组与元组 ​

8.1 数组(Array) ​

定义 ​

数组类型表示同类型元素的集合。有两种表示方式:T[] 和 Array<T>。

typescript
let list1: number[] = [1, 2, 3];
let list2: Array<number> = [1, 2, 3];
let matrix: number[][] = [[1, 2], [3, 4]];

8.2 元组(Tuple) ​

定义 ​

元组类型表示固定长度、固定元素类型的数组,每个位置的类型可以不同。

typescript
// 元组 - 固定长度、固定类型的数组
let pair: [string, number] = ['Alice', 30];
const [name, age] = pair;

type Color = [number, number, number, number?];
const red: Color = [255, 0, 0];
const semiRed: Color = [255, 0, 0, 0.5];

对比:数组 vs 元组 ​

对比维度数组(Array)元组(Tuple)
定义同类型元素的集合,长度可变固定长度、固定类型,各位置类型可不同
语法type[] 或 Array<type>[type1, type2, ...]
元素类型所有元素类型相同每个位置的类型可不同
长度可变固定(可选元素除外)
使用场景列表数据、集合数据精确返回多个值、CSV 行、React useState

相同点:底层都是 JavaScript 数组,运行时没有区别。

不同点:

  • 元组在编译期有更严格的类型检查(固定长度、固定位置类型)
  • 数组适合"同类型数据的列表",元组适合"不同类型数据的组合"
typescript
// 实际场景:React useState 返回元组
// const [count, setCount] = useState<number>(0);

// 实际场景:CSV 解析
type CsvRow = [id: number, name: string, email: string, age: number];

注意事项:

元组的 .push() 不会被阻止:TypeScript 不会阻止你往元组中 push 额外的元素。这属于类型系统的已知限制。

使用 as const 推断元组:

typescript
const args = [1, 'hello'] as const; // 推断为 readonly [1, 'hello']

9. 函数 ​

定义 ​

函数类型在 TypeScript 中可以标注参数类型和返回值类型,也可以定义函数类型表达式。

函数类型表达式 ​

typescript
type Greet = (name: string) => string;
const greet: Greet = (name) => `Hello, ${name}!`;

可选参数与默认参数 ​

typescript
function createUser(name: string, age?: number): void {
  console.log(`${name}${age !== undefined ? `, ${age} years old` : ''}`);
}

function createUrl(base: string, port: number = 443): string {
  return `${base}:${port}`;
}

function sum(...numbers: number[]): number {
  return numbers.reduce((acc, n) => acc + n, 0);
}

函数重载 ​

定义:同一个函数可以有多个类型签名,TypeScript 根据传入参数的类型/数量选择正确的签名。

typescript
function makeDate(timestamp: number): Date;
function makeDate(year: number, month: number, day: number): Date;
function makeDate(yearOrTimestamp: number, month?: number, day?: number): Date {
  if (month !== undefined && day !== undefined) {
    return new Date(yearOrTimestamp, month - 1, day);
  }
  return new Date(yearOrTimestamp);
}

对比:可选参数 vs 默认参数 ​

对比维度可选参数默认参数
定义调用时可省略的参数省略时使用默认值的参数
语法param?: Typeparam: Type = value
类型推断param?: string → 类型为 string | undefinedparam: string = 'hello' → 类型为 string
内部访问需要检查是否为 undefined直接使用,一定为声明的类型
使用场景参数不是每次都需要参数有合理的默认值

相同点:调用时都可以省略。

不同点:

  • 默认参数不需要在函数体内检查 undefined
  • 默认参数不会计入函数的 .length 属性(.length 只统计第一个默认参数之前的形参数量)
  • 可选参数内部类型是 Type | undefined,默认参数内部类型是 Type

注意事项:

可选参数必须在必需参数之后:function f(a?: number, b: number) 是不允许的。

默认参数不一定在必需参数之后:function f(a: number, b: string = 'hello', c: number) 是允许的。

函数重载只有声明(overload signatures)是公开的类型信息,实现签名(implementation signature)的类型不会被外部看到。实现签名的参数类型应该足够包容,能覆盖所有重载声明的参数组合。


10. 对象类型 ​

定义 ​

对象类型描述一个对象的结构:有哪些属性、每个属性的类型。可以内联定义,也可以通过 interface 或 type 定义。

typescript
function printCoord(pt: { x: number; y: number }): void {
  console.log(`(${pt.x}, ${pt.y})`);
}

readonly 属性 ​

typescript
interface Home {
  readonly address: string;
  readonly owner: string;
}

索引签名 ​

定义:用于描述具有动态键名的对象,规定键和值的类型。

typescript
interface StringDictionary {
  [key: string]: string;
}

分层实体模型 ​

typescript
// 实际场景:分层的实体模型
interface BaseEntity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

interface UserEntity extends BaseEntity {
  name: string;
  email: string;
  role: 'admin' | 'user';
}

注意事项 ​

索引签名与普通属性冲突:如果使用了索引签名 [key: string]: string,所有普通属性的值类型也必须兼容 string:

typescript
interface Dictionary {
  [key: string]: string;
  id: number; // ❌ 错误:number 不兼容 string
}

解决方法:使用联合类型 [key: string]: string | number。

属性可选性严格性:当赋值一个对象字面量时,TypeScript 会进行多余属性检查(Excess Property Checking),防止传入未声明的属性。但在函数调用或变量赋值时,这种检查不会发生(结构类型兼容性)。


11. 泛型(Generics) ​

定义 ​

泛型(Generics)是 TypeScript 最强大的特性之一,让你编写可重用的、类型安全的组件,同时保持对具体类型的精确控制。

基础泛型 ​

typescript
function identity<T>(arg: T): T {
  return arg;
}

const output1 = identity<string>('hello');
const output2 = identity(42); // 推断为 number

interface Box<T> {
  value: T;
}
const numberBox: Box<number> = { value: 42 };

泛型约束与 keyof ​

typescript
interface HasLength { length: number; }

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

泛型缓存类 ​

typescript
// 实际场景:泛型缓存类
class Cache<T> {
  private store = new Map<string, { data: T; expiresAt: number }>();

  set(key: string, data: T, ttlMs: number = 60000): void {
    this.store.set(key, { data, expiresAt: Date.now() + ttlMs });
  }

  get(key: string): T | undefined {
    const entry = this.store.get(key);
    if (!entry || entry.expiresAt < Date.now()) {
      this.store.delete(key);
      return undefined;
    }
    return entry.data;
  }
}

// API 响应包装
async function apiGet<T>(url: string): Promise<T> {
  const response = await fetch(url);
  if (!response.ok) throw new Error(`API error: ${response.status}`);
  return response.json();
}

注意事项 ​

泛型可以理解为"类型的参数"。就像函数参数让你能接收不同的值,泛型让你能接收不同的类型。

泛型的数量越少越好。如果一个函数只需要一个类型参数,就不要用两个。过度使用泛型会降低代码可读性。

泛型约束(extends)不是运行时约束。<T extends HasLength> 只在编译期确保 T 具有 length 属性,运行时不会检查。

当泛型没有被使用时:如果泛型函数被调用时没有显式指定类型,TypeScript 会基于实际参数推断。如果无法推断,可能会回退为一个空对象类型或 unknown。

对比:泛型 vs any ​

对比维度泛型(Generic)any
定义保留类型关系的类型参数跳过所有类型检查
类型安全✅ 完全安全❌ 不安全
输入输出关系✅ 保留(T → T)❌ 失去关系
可读性稍差(需要理解类型参数)好(但无有用信息)
推荐度✅ 优先使用❌ 尽量避免

12. 枚举(Enums) ​

定义 ​

枚举(Enum)是一组有名字的常量集合,TypeScript 支持数字枚举和字符串枚举。

数字枚举 ​

typescript
enum Direction {
  Up,    // 0
  Down,  // 1
  Left,  // 2
  Right  // 3
}

字符串枚举 ​

typescript
enum Color {
  Red = 'RED',
  Green = 'GREEN',
  Blue = 'BLUE'
}

实际场景 ​

typescript
// 实际场景:订单状态
enum OrderStatus {
  Pending = 'pending',
  Confirmed = 'confirmed',
  Shipping = 'shipping',
  Delivered = 'delivered',
  Cancelled = 'cancelled'
}

function getStatusColor(status: OrderStatus): string {
  const colors: Record<OrderStatus, string> = {
    [OrderStatus.Pending]: 'yellow',
    [OrderStatus.Confirmed]: 'blue',
    [OrderStatus.Shipping]: 'cyan',
    [OrderStatus.Delivered]: 'green',
    [OrderStatus.Cancelled]: 'red',
  };
  return colors[status];
}

对比:枚举 vs 联合类型(字符串字面量) ​

对比维度枚举(Enum)联合类型(Union Type)
定义一组具名常量集合一组字面量类型的联合
运行时存在✅ 存在(编译为对象)❌ 不存在(编译时擦除)
反向映射✅ 数字枚举支持❌ 不支持
可遍历✅ 可以遍历枚举成员❌ 不能遍历
性能有运行时开销(生成 JS 对象)无运行时开销
推荐度视情况✅ 多数场景优先使用

相同点:都可以表示一组固定的可选值。

不同点:

  • 枚举在运行时存在,会编译为 JavaScript 对象
  • 联合类型是纯编译时的概念,运行时被擦除为具体的值
  • 枚举支持反向映射(通过值找名称),联合类型不支持
  • 枚举需要先定义再使用,联合类型更灵活

注意事项:

枚举是 TypeScript 中少数有运行时行为的特性之一。大部分 TS 特性(接口、类型别名等)都是编译时擦除的,但枚举会编译为 JavaScript 对象。

避免使用数字枚举的非初始化成员作为 ${} 中的值:如果不小心依赖了数字枚举的自动递增行为,当中间插入新成员时,所有后续成员的数值都会变化,导致潜在 bug。

优先使用字符串枚举或联合类型:字符串枚举的行为更可预测,联合类型则没有运行时开销。


13. 类(Classes) ​

定义 ​

TypeScript 的类在 JavaScript 类的基础上增加了类型注解、访问修饰符(public/private/protected)、抽象类等特性。

基础用法 ​

typescript
class Person {
  constructor(public name: string, private age: number) {}

  greet(): string {
    return `Hi, I'm ${this.name} and I'm ${this.age} years old.`;
  }
}

Getter / Setter ​

typescript
class Temperature {
  private _celsius: number = 0;

  get celsius(): number { return this._celsius; }

  set celsius(value: number) {
    if (value < -273.15) throw new Error('温度不能低于绝对零度');
    this._celsius = value;
  }
}

继承 ​

typescript
class Animal {
  constructor(public name: string) {}
  speak(): string { return `${this.name} makes a sound.`; }
}

class Dog extends Animal {
  constructor(name: string, public breed: string) { super(name); }
  speak(): string { return `${this.name} barks!`; }
}

implements 关键字 ​

typescript
interface ILogger {
  log(message: string): void;
  error(message: string): void;
}

class ConsoleLogger implements ILogger {
  log(message: string): void { console.log(`[LOG] ${message}`); }
  error(message: string): void { console.error(`[ERROR] ${message}`); }
}

对比:public vs private vs protected vs readonly ​

修饰符定义类内部访问子类访问外部访问
public公有,默认访问级别✅✅✅
private私有,仅类内部✅❌❌
protected受保护,类内部及子类✅✅❌
readonly只读,初始化后不可修改✅(不可赋值)✅(不可赋值)✅(不可赋值)

相同点:private、protected、readonly 都是访问控制相关的修饰符。

不同点:

  • private 最严格,只有定义它的类可以访问
  • protected 允许子类访问,但外部不行
  • readonly 控制的是"可写性"而非"可见性"——可以读取但不能赋值
  • readonly 可以与其他修饰符组合使用(如 public readonly、private readonly)

注意事项:

TypeScript 的 private 是编译期的,不是运行时。在编译后的 JavaScript 中,private 字段只是普通属性。TypeScript 3.8+ 支持了 JavaScript 原生 # 私有字段(真正运行时私有)。

readonly 与 const 的区别:

  • const 用于变量,确保变量引用不变
  • readonly 用于属性,确保属性值不变 两者都只在编译期生效。

一句话原则:纯数据结构用 interface/type,需要封装状态和行为的用 class。

对比:extends vs implements ​

对比维度extends(继承)implements(实现)
定义子类继承父类的实现类承诺满足接口的形状
可作用于类或接口接口
继承实现✅ 继承属性和方法实现❌ 不继承实现,只做类型检查
多重使用单继承(一个父类)可实现多个接口
super() 调用需要调用 super()不需要

14. 类型缩小(Narrowing) ​

定义 ​

类型缩小(Narrowing)是 TypeScript 在代码分支中将宽类型变为窄类型的过程。通过类型守卫,TypeScript 能推断出在特定代码分支中的变量比之前声明的类型更精确。

typeof 类型守卫 ​

typescript
function printValue(value: string | number | boolean): void {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else if (typeof value === 'number') {
    console.log(value.toFixed(2));
  } else {
    console.log(value ? 'true' : 'false');
  }
}

可辨识联合(Discriminated Union) ​

定义:这是 TypeScript 最强大的模式之一。通过联合类型每个成员共有的字面量属性作为"标签",在 switch/case 或 if 分支中精确缩小类型。

typescript
type ApiState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

function handleState<T>(state: ApiState<T>): string {
  switch (state.status) {
    case 'loading':
      return '加载中...';
    case 'success':
      return `数据: ${JSON.stringify(state.data)}`;
    case 'error':
      return `错误: ${state.error}`;
  }
}

// 实际场景:React 请求状态管理
type AsyncState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string };

类型谓词(Type Predicates) ​

定义:类型谓词(Type Predicates)是一个返回类型为 paramName is Type 的函数,它告诉 TypeScript:如果这个函数返回 true,参数就是指定的类型。

typescript
interface Fish { name: string; swim(): void; }
interface Bird { name: string; fly(): void; }

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

const pets: (Fish | Bird)[] = [];
const fishOnly = pets.filter(isFish); // 类型为 Fish[]

对比:类型缩小 vs 类型断言 ​

对比维度类型缩小(Narrowing)类型断言(Type Assertion)
定义TypeScript 自动收窄类型开发者手动告诉 TS 一个更具体的类型
安全性✅ 安全(TS 验证逻辑)⚠️ 不安全(开发者负责)
语法typeof、instanceof、可辨识联合as Type 或 <Type>
适用范围分支语句中(if/switch)任何表达式位置
推荐度✅ 优先使用⚠️ 仅当开发者比 TS 更了解类型时使用

不同点:

  • 类型缩小是 TS 编译器自动完成的,基于你的代码逻辑
  • 类型断言是你手动覆盖 TS 的类型判断,可能出错
  • 类型断言不进行任何运行时检查,只是编译时的类型转换

注意事项:

类型断言不是类型转换。它不会改变变量的运行时值,只是告诉编译器"请把我当这个类型处理"。运行时值不会改变。

可辨识联合的判别式必须是字面量类型(如字符串字面量、数字字面量),不能是 string 类型的普通属性。


15. 类型操作 ​

15.1 keyof 操作符 ​

定义:keyof T 返回类型 T 所有公共属性名的联合类型。

typescript
interface User {
  id: number;
  name: string;
  email: string;
}

type UserKeys = keyof User; // 'id' | 'name' | 'email'

function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

15.2 typeof 操作符 ​

定义:typeof 在类型上下文中取一个值(变量、属性等)的类型。

typescript
const config = { apiUrl: 'https://api.example.com', timeout: 5000, retry: true };
type Config = typeof config;
// { apiUrl: string; timeout: number; retry: boolean; }

const colors = ['red', 'green', 'blue'] as const;
type Color = (typeof colors)[number]; // 'red' | 'green' | 'blue'

15.3 条件类型(Conditional Types) ​

定义:形如 T extends U ? X : Y 的类型表达式,根据条件选择不同的类型分支。

typescript
type IsString<T> = T extends string ? 'yes' : 'no';
type A = IsString<string>; // 'yes'
type B = IsString<number>; // 'no'

// infer 关键字 - 从类型中提取子类型
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type Unwrapped = Unwrap<Promise<string>>; // string

15.4 映射类型(Mapped Types) ​

定义:通过遍历联合类型的成员来生成新类型。语法:{ [K in keyof T]: NewType }。

typescript
type Stringify<T> = {
  [K in keyof T]: string;
};

interface User { id: number; name: string; age: number; }
type StringifiedUser = Stringify<User>;
// { id: string; name: string; age: string; }

// 键名重映射(TS 4.1+)
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

15.5 模板字面量类型 ​

定义:通过模板字符串语法,从字符串字面量类型生成新的字符串字面量类型。

typescript
type Vertical = 'top' | 'bottom';
type Horizontal = 'left' | 'right';
type Position = `${Vertical}-${Horizontal}`;
// 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right'

type CssSize = `${number}px` | `${number}rem` | `${number}%` | 'auto';

对比:keyof vs in ​

对比维度keyofin
定义获取对象类型的所有键的联合类型在映射类型中遍历联合类型成员
来源来自类型来自联合类型(可以是 keyof 的结果)
使用位置类型层面(type、泛型约束等)映射类型的索引签名中
典型用法keyof T 得到键的联合[K in keyof T] 遍历所有键

相同点:两者经常一起使用,在映射类型中 [K in keyof T] 是标准写法。

不同点:

  • keyof 是获取键集合的操作符
  • in 是遍历操作符,用于映射类型
  • keyof 可以单独使用,in 通常与 keyof 或其他联合类型配合使用

对比:typeof(运行时)vs typeof(类型层) ​

⚠️ 注意:JavaScript 中的 typeof 和 TypeScript 中的 typeof 是两个不同的东西!

对比维度JS 运行时 typeofTS 类型层 typeof
作用返回值的类型字符串获取值的 TypeScript 类型
出现位置运行时表达式类型注解/类型表达式
返回值'string'、'number' 等字符串TypeScript 类型
示例typeof 'hello' === 'string'type T = typeof someValue

相同点:语法一样(typeof x)。

不同点:

  • 运行时 typeof 返回的是 JavaScript 的 7 种基本类型字符串
  • 类型层的 typeof 返回的是完整的 TypeScript 类型(包括 interface、自定义类型等)

16. 工具类型(Utility Types) ​

定义 ​

TypeScript 内置了一系列工具类型(Utility Types),它们是基于条件类型、映射类型等高级特性预先实现的常用类型转换函数。

typescript
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

Partial<T> ​

定义:将 T 的所有属性变为可选。

typescript
// Partial - 所有属性可选
function updateUser(id: number, updates: Partial<User>): void {}
updateUser(1, { name: 'Alice' }); // 只需传需要修改的字段

Required<T> ​

定义:将 T 的所有可选属性变为必选。

typescript
type RequiredConfig = Required<{ apiUrl?: string; timeout?: number }>;
// { apiUrl: string; timeout: number; }

Readonly<T> ​

定义:将 T 的所有属性变为只读。

typescript
const point: Readonly<{ x: number; y: number }> = { x: 10, y: 20 };
// point.x = 5; // ❌ 错误

Record<K, T> ​

定义:创建键类型为 K、值类型为 T 的对象类型。

typescript
type Page = 'home' | 'about' | 'contact';
type PageInfo = Record<Page, { title: string; url: string }>;
// { home: { title: string; url: string }; about: { ... }; contact: { ... } }

Pick<T, K> ​

定义:从 T 中选取一组属性 K 构成新类型。

typescript
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;

Omit<T, K> ​

定义:从 T 中排除一组属性 K 构成新类型。

typescript
type CreateUserInput = Omit<User, 'id'>;

Parameters<T> / ReturnType<T> ​

定义:

  • Parameters<T>:提取函数类型 T 的参数类型(元组)
  • ReturnType<T>:提取函数类型 T 的返回类型
typescript
function greet(name: string, age: number): string { return ''; }
type GreetParams = Parameters<typeof greet>; // [name: string, age: number]
type GreetReturn = ReturnType<typeof greet>; // string

NonNullable<T> ​

定义:从联合类型中排除 null 和 undefined。

typescript
type T0 = NonNullable<string | number | undefined>; // string | number

对比:Partial vs Pick vs Omit ​

对比维度Partial<T>Pick<T, K>Omit<T, K>
定义所有属性变为可选选取部分属性排除部分属性
适用场景更新操作的参数公开部分属性排除敏感字段
控制方式全部(要么全可选)指定保留的键指定排除的键
示例Partial<User>Pick<User, 'id' | 'name'>Omit<User, 'password'>

相同点:都是从已有类型派生出新类型,不会修改原类型。

不同点:

  • Partial 改变属性的"必要性"(必选→可选)
  • Pick 通过"保留"的方式筛选属性
  • Omit 通过"排除"的方式筛选属性
  • Pick 和 Omit 是互补的:Pick<T, K> = Omit<T, 除了K以外的所有键>

对比:Parameters vs ReturnType ​

对比维度Parameters<T>ReturnType<T>
定义提取函数参数类型提取函数返回类型
结果元组类型直接类型
典型用法推导回调参数推导函数返回值
联合函数所有参数签名的交叉所有返回类型的联合

17. 模块系统 ​

定义 ​

TypeScript 支持 ES Module(ESM)模块系统,通过 import 和 export 关键字组织代码。

typescript
// math.ts
export function add(a: number, b: number): number {
  return a + b;
}

export const PI = 3.14159;

// main.ts
import { add, PI } from './math';

// 默认导出
export default class Logger {
  log(msg: string) { console.log(msg); }
}

// tsconfig.json 配置
// {
//   'compilerOptions': {
//     'module': 'ESNext',
//     'moduleResolution': 'bundler',
//     'baseUrl': '.',
//     'paths': { '@/*': ['src/*'] }
//   }
// }

注意事项 ​

module 和 moduleResolution 必须匹配:

  • 如果你的目标是 Node.js 环境,现代项目用 "module": "NodeNext" + "moduleResolution": "nodenext"(旧项目可继续用 "module": "CommonJS" + "moduleResolution": "node")
  • 如果你的项目用打包工具(Vite、Webpack),用 "module": "ESNext" + "moduleResolution": "bundler"
  • 不匹配的配置会导致模块解析失败

路径别名需要打包工具配合:tsconfig.json 的 paths 配置只影响 TypeScript 编译,运行时还需要打包工具的 resolve 配置(如 Vite 的 resolve.alias、Webpack 的 resolve.alias)。


18. 声明文件 ​

定义 ​

声明文件(.d.ts 文件)描述了 JavaScript 库或模块的类型信息,让 TypeScript 能对纯 JS 代码进行类型检查。

typescript
// globals.d.ts
declare const VERSION: string;

// env.d.ts
/// <reference types='vite/client' />

declare module '*.vue' {
  import type { DefineComponent } from 'vue';
  const component: DefineComponent<{}, {}, any>;
  export default component;
}

实际场景 ​

先在 npm 查找 @types/ 包:

bash
npm install --save-dev @types/lodash

注意事项 ​

declare 关键字不产生实现。它只是告诉 TypeScript 某个值存在,但不在编译后的 JS 中生成任何代码。

三斜线指令(/// <reference>)只应在声明文件中使用。在普通 TS 文件中应使用 import。

社区优先:遇到没有类型声明的库,先搜索 @types/<库名>。如果不存在,再考虑自己写声明文件。写声明文件时尽量只描述公共 API 的类型,不要追求覆盖所有内部实现细节。


19. 装饰器(Decorators) ​

一、简介 ​

  1. 装饰器本质是一种特殊的函数,它可以对:类、属性、方法、参数进行扩展,同时能让代码更简洁。
  2. 装饰器的提案自 2015 年在 TC39 中提出至今已超过十年,但它从未进入 ES6/ES2015 标准,目前仍处于 TC39 Stage 3 阶段(尚未正式成为 ECMAScript 标准)。
  3. TypeScript 5.0 起已原生支持 TC39 标准装饰器(类/方法/访问器/字段装饰器),无需 experimentalDecorators;只有参数装饰器仍属于旧版(legacy)实验性语法,需开启 experimentalDecorators 才能使用。
  4. 装饰器有 5 种:

1⃣类装饰器 2⃣属性装饰器 3⃣方法装饰器 4⃣访问器装饰器 5⃣参数装饰器

备注:TypeScript 5.0 起可直接使用 TC39 标准装饰器(类、方法、访问器、字段装饰器),无需任何配置;标准装饰器不支持参数装饰器。本章下文介绍的是旧版(legacy)experimentalDecorators 语法(含参数装饰器),若使用旧版语法,需在 tsconfig 中开启 experimentalDecorators。新旧两套语法互不兼容。 参考:《TypeScript 5.0发版公告》

二、类装饰器 ​

基本语法 ​

说明: 类装饰器是一个应用在类声明上的函数,可以为类添加额外的功能,或添加额外的逻辑。

typescript
/* 
  Demo函数会在Person类定义时执行
  参数说明:
    ○ target参数是被装饰的类,即:Person
*/
function Demo(target: Function) {
  console.log(target)
}

// 使用装饰器
@Demo
class Person { }

应用举例 ​

提示: 需求:定义一个装饰器,实现Person实例调用toString时返回JSON.stringify的执行结果。

typescript
// 使用装饰器重写toString方法 + 封闭其原型对象
function CustomString(target: Function) {
  // 向被装饰类的原型上添加自定义的 toString 方法
  target.prototype.toString = function () {
    return JSON.stringify(this)
  }
  // 封闭其原型对象,禁止随意操作其原型对象
  Object.seal(target.prototype)
}

// 使用 CustomString 装饰器
@CustomString
class Person {
  constructor(public name: string, public age: number) { }
  speak() {
    console.log('你好呀!')
  }
}

/* 测试代码如下 */
let p1 = new Person('张三', 18)
// 输出:{"name":"张三","age":18}
console.log(p1.toString())
// 禁止随意操作其原型对象
interface Person {
  a: any
}
// Person.prototype.a = 100 // 此行会报错:Cannot add property a, object is not extensible
// console.log(p1.a)

关于返回值 ​

说明:类装饰器有返回值:若类装饰器返回一个新的类,那这个新类将替换掉被装饰的类。 类装饰器无返回值:若类装饰器无返回值或返回undefined,那被装饰的类不会被替换。

typescript
function demo(target:Function){
  // 装饰器有返回值时,该返回值会替换掉被装饰的类
  return class {
    test(){
      console.log(200)
      console.log(300)
      console.log(400)
    }
  }
}

@demo
class Person {
  test(){
    console.log(100)
  }
}

console.log(Person)

关于构造类型 ​

在 TypeScript 中,Function 类型所表示的范围十分广泛,包括:普通函数、箭头函数、方法等等。但并非Function 类型的函数都可以被 new 关键字实例化,例如箭头函数是不能被实例化的,那么 TypeScript 中概如何声明一个构造类型呢?有以下两种方式:

typescript
/*
  ○ new     表示:该类型是可以用new操作符调用。
  ○ ...args 表示:构造器可以接受【任意数量】的参数。
  ○ any[]   表示:构造器可以接受【任意类型】的参数。
  ○ {}      表示:返回类型是对象(非null、非undefined的对象)。
*/

// 定义Constructor类型,其含义是构造类型
type Constructor = new (...args: any[]) => {};

function test(fn:Constructor){}
class Person {}
test(Person)
typescript
// 定义一个构造类型,且包含一个静态属性 wife
type Constructor = {
  new(...args: any[]): {}; // 构造签名
  wife: string; // wife属性
};

function test(fn:Constructor){}
class Person {
  static wife = 'asd'
}
test(Person)

替换被装饰的类 ​

对于高级一些的装饰器,不仅仅是覆盖一个原型上的方法,还要有更多功能,例如添加新的方法和状态。

提示: 需求:设计一个LogTime装饰器,可以给实例添加一个属性,用于记录实例对象的创建时间,再添加一个方法用于读取创建时间。

typescript
// User接口
interface User {
  getTime(): Date
  log(): void
}

// 自定义类型Class
type Constructor = new (...args: any[]) => {}

// 创建一个装饰器,为类添加日志功能和创建时间
function LogTime<T extends Constructor>(target: T) {
  return class extends target {
    createdTime: Date;
    constructor(...args: any[]) {
      super(...args);
      this.createdTime = new Date(); // 记录对象创建时间
    }
    getTime() {
      return `该对象创建时间为:${this.createdTime}`;
    }
  };
}

@LogTime
class User {
  constructor(
    public name: string,
    public age: number
  ) { }
  speak() {
    console.log(`${this.name}说:你好啊!`)
  }
}

const user1 = new User('张三', 13);
user1.speak()
console.log(user1.getTime())

三、装饰器工厂 ​

装饰器工厂是一个返回装饰器函数的函数,可以为装饰器添加参数,可以更灵活地控制装饰器的行为。

提示: 需求**:**定义一个LogInfo类装饰器工厂,实现Person实例可以调用到introduce方法,且introduce中输出内容的次数,由LogInfo接收的参数决定。

typescript
interface Person {
  introduce: () => void
}

// 定义一个装饰器工厂 LogInfo,它接受一个参数 n,返回一个类装饰器
function LogInfo(n:number) {
  // 装饰器函数,target 是被装饰的类
  return function(target: Function){
    target.prototype.introduce = function () {
      for (let i = 0; i < n; i++) {
        console.log(`我的名字:${this.name},我的年龄:${this.age}`)
      }
    }
  }
}

@LogInfo(5)
class Person {
  constructor(
    public name: string,
    public age: number
  ) { }
  speak() {
    console.log('你好呀!')
  }
}

let p1 = new Person('张三', 18)
// console.log(p1) // 打印的p1是:_classThis,转换的JS版本比较旧时,会出现,不必纠结
p1.speak()
p1.introduce()

四、装饰器组合 ​

装饰器可以组合使用,执行顺序为:先【由上到下】的执行所有的装饰器工厂,依次获取到装饰器,然后再【由下到上】执行所有的装饰器。

typescript
//装饰器
function test1(target:Function) {
  console.log('test1')
}
//装饰器工厂
function test2() {
  console.log('test2工厂')
  return function (target:Function) { 
    console.log('test2')
  }
}
//装饰器工厂
function test3() {
  console.log('test3工厂')
  return function (target:Function) { 
    console.log('test3')
  }
}
//装饰器
function test4(target:Function) {
  console.log('test4')
}

@test1
@test2()
@test3()
@test4
class Person { }

/*
  控制台打印:
    test2工厂
    test3工厂
    test4
    test3
    test2
    test1
*/
typescript
// 自定义类型Class
type Constructor = new (...args: any[]) => {}

interface Person {
  introduce():void
  getTime():void
}

// 使用装饰器重写toString方法 + 封闭其原型对象
function customToString(target: Function) {
  // 向被装饰类的原型上添加自定义的 toString 方法
  target.prototype.toString = function () {
    return JSON.stringify(this)
  }
  // 封闭其原型对象,禁止随意操作其原型对象
  Object.seal(target.prototype)
}

// 创建一个装饰器,为类添加日志功能和创建时间
function LogTime<T extends Constructor>(target: T) {
  return class extends target {
    createdTime: Date;
    constructor(...args: any[]) {
      super(...args);
      this.createdTime = new Date(); // 记录对象创建时间
    }
    getTime() {
      return `该对象创建时间为:${this.createdTime}`;
    }
  };
}

// 定义一个装饰器工厂 LogInfo,它接受一个参数 n,返回一个类装饰器
function LogInfo(n:number) {
  // 装饰器函数,target 是被装饰的类
  return function(target: Function){
    target.prototype.introduce = function () {
      for (let i = 0; i < n; i++) {
        console.log(`我的名字:${this.name},我的年龄:${this.age}`)
      }
    }
  }
}

@customToString
@LogInfo(3)
@LogTime
class Person {
  constructor(
    public name: string,
    public age: number
  ) { }
  speak() {
    console.log('你好呀!')
  }
}

const p1 = new Person('张三',18)
console.log(p1.toString())
p1.introduce()
console.log(p1.getTime())

五、属性装饰器 ​

基本语法 ​

typescript
/* 
  参数说明:
    ○ target: 对于静态属性来说值是类,对于实例属性来说值是类的原型对象。
    ○ propertyKey: 属性名。
*/
function Demo(target: object, propertyKey: string) {
  console.log(target,propertyKey)
}

class Person {
  @Demo name: string
  @Demo age: number
  @Demo static school:string

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

const p1 = new Person('张三', 18)

关于属性遮蔽 ​

如下代码中:当构造器中的this.age = age试图在实例上赋值时,实际上是调用了原型上age属性的set方法。

typescript
class Person {
  name: string
  age: number
  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

let value = 99
// 使用defineProperty给Person原型添加age属性,并配置对应的get与set
Object.defineProperty(Person.prototype, 'age', {
  get() {
    return value
  },
  set(val) {
    value = val
  }
})

const p1 = new Person('张三', 18)
console.log(p1.age) //18
console.log(Person.prototype.age)//18

应用举例 ​

提示: 需求:定义一个State属性装饰器,来监视属性的修改。

typescript
// 声明一个装饰器函数 State,用于捕获数据的修改
function State(target: object, propertyKey: string) {
  // 存储属性的内部值
  let key = `__${propertyKey}`;

  // 使用 Object.defineProperty 替换类的原始属性
  // 重新定义属性,使其使用自定义的 getter 和 setter
  Object.defineProperty(target, propertyKey, {
    get () {
      return this[key]
    },
    set(newVal: string){
      console.log(`${propertyKey}的最新值为:${newVal}`);
      this[key] = newVal
    },
    enumerable: true, 
    configurable: true,
  });
}

class Person {
  name: string;
  //使用State装饰器
  @State age: number;
  school = 'atguigu';
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

const p1 = new Person('张三', 18);
const p2 = new Person('李四', 30);

p1.age = 80
p2.age = 90

console.log('------------------')
console.log(p1.age) //80
console.log(p2.age) //90

六、方法装饰器 ​

基本语法 ​

typescript
/* 
  参数说明:
    ○ target: 对于静态方法来说值是类,对于实例方法来说值是原型对象。
    ○ propertyKey:方法的名称。
    ○ descriptor: 方法的描述对象,其中value属性是被装饰的方法。
*/
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor){
  console.log(target)
  console.log(propertyKey)
  console.log(descriptor)
}

class Person {
  constructor(
    public name:string,
    public age:number,
  ){}
  // Demo装饰实例方法
  @Demo speak(){
    console.log(`你好,我的名字:${this.name},我的年龄:${this.age}`)
  }
  // Demo装饰静态方法
  @Demo static isAdult(age:number) {
    return age >= 18;
  }
}

const p1 = new Person('张三',18)
p1.speak()

应用举例 ​

提示: 需求:

  1. 定义一个Logger方法装饰器,用于在方法执行前和执行后,均追加一些额外逻辑。

  2. 定义一个Validate方法装饰器,用于验证数据。

    typescript
    function Logger(target: object, propertyKey: string, descriptor: PropertyDescriptor){
    // 保存原始方法
    const original = descriptor.value;
    // 替换原始方法
    descriptor.value = function (...args:any[]) {
     console.log(`${propertyKey}开始执行......`)
     const result = original.call(this, ...args)
     console.log(`${propertyKey}执行完毕......`)
     return result;
    }
    }

function Validate(maxValue:number){ return function (target: object, propertyKey: string, descriptor: PropertyDescriptor){ // 保存原始方法 const original = descriptor.value; // 替换原始方法 descriptor.value = function (...args: any[]) { // 自定义的验证逻辑 if (args[0] > maxValue) { throw new Error('年龄非法!') } // 如果所有参数都符合要求,则调用原始方法 return original.apply(this, args); }; } }

class Person { constructor( public name:string, public age:number, ){} @Logger speak(){ console.log(你好,我的名字:${this.name},我的年龄:${this.age}) } @Validate(120) static isAdult(age:number) { return age >= 18; } }

const p1 = new Person('张三',18) p1.speak() console.log(Person.isAdult(100))

### 七、访问器装饰器
#### 基本语法
```typescript
/* 
  参数说明:
    ○ target: 
        1. 对于实例访问器来说值是【所属类的原型对象】。
        2. 对于静态访问器来说值是【所属类】。
    ○ propertyKey:访问器的名称。
    ○ descriptor: 描述对象。
*/
function Demo(target: object, propertyKey: string, descriptor: PropertyDescriptor) {
  console.log(target)
  console.log(propertyKey)
  console.log(descriptor)
}

class Person {
  @Demo
  get address(){
    return '北京宏福科技园'
  }
  @Demo
  static get country(){
    return '中国'
  }
}

应用举例 ​

提示: 需求:对Weather类的temp属性的set访问器进行限制,设置的最低温度-50,最高温度50

typescript
function RangeValidate(min: number, max: number) {
  return function (target: object, propertyKey: string, descriptor: PropertyDescriptor) {
    // 保存原始的 setter 方法,以便在后续调用中使用
    const originalSetter = descriptor.set;

    // 重写 setter 方法,加入范围验证逻辑
    descriptor.set = function (value: number) {
      // 检查设置的值是否在指定的最小值和最大值之间
      if (value < min || value > max) {
        // 如果值不在范围内,抛出错误
        throw new Error(`${propertyKey}的值应该在 ${min} 到 ${max}之间!`);
      }

      // 如果值在范围内,且原始 setter 方法存在,则调用原始 setter 方法
      if (originalSetter) {
        originalSetter.call(this, value);
      }
    };
  };
}

class Weather {
  private _temp: number;
  constructor(_temp: number) {
    this._temp = _temp;
  }
  // 设置温度范围在 -50 到 50 之间
  @RangeValidate(-50,50) 
  set temp(value) {
    this._temp = value;
  }
  get temp() {
    return this._temp;
  }
}

const w1 = new Weather(25);
console.log(w1)
w1.temp = 67
console.log(w1)

八、参数装饰器 ​

基本语法 ​

typescript
/* 
  参数说明:
    ○ target:
      1.如果修饰的是【实例方法】的参数,target 是类的【原型对象】。
      2.如果修饰的是【静态方法】的参数,target 是【类】。
    ○ propertyKey:参数所在的方法的名称。
    ○ parameterIndex: 参数在函数参数列表中的索引,从 0 开始。
*/
function Demo(target: object, propertyKey: string, parameterIndex: number) {
  console.log(target)
  console.log(propertyKey)
  console.log(parameterIndex)
}

// 类定义
class Person {
  constructor(public name: string) { }
  speak(@Demo message1: any, mesage2: any) {
    console.log(`${this.name}想对说:${message1},${mesage2}`);
  }
}

应用举例 ​

提示: 需求:定义方法装饰器Validate,同时搭配参数装饰器NotNumber,来对speak方法的参数类型进行限制。

typescript
function NotNumber(target: any, propertyKey: string, parameterIndex: number) {
  // 初始化或获取当前方法的参数索引列表
  let notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
  // 将当前参数索引添加到列表中
  notNumberArr.push(parameterIndex);
  // 将列表存储回目标对象
  target[`__notNumber_${propertyKey}`] = notNumberArr;
}

// 方法装饰器定义
function Validate(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
  const method = descriptor.value;
  descriptor.value = function (...args: any[]) {
    // 获取被标记为不能为空的参数索引列表
    const notNumberArr: number[] = target[`__notNumber_${propertyKey}`] || [];
    // 检查参数是否为 null 或 undefined
    for (const index of notNumberArr) {
      if (typeof args[index] === 'number') {
        throw new Error(`方法 ${propertyKey} 中索引为 ${index} 的参数不能是数字!`)
      }
    }
    // 调用原始方法
    return method.apply(this, args);
  };

  return descriptor;
}

// 类定义
class Student {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
  @Validate
  speak(@NotNumber message1: any, mesage2: any) {
    console.log(`${this.name}想对说:${message1},${mesage2}`);
  }
}

// 使用
const s1 = new Student("张三");
s1.speak(100, 200);

九、TC39 标准装饰器(TypeScript 5.0+)与 legacy 语法完整对照 ​

上文(二~八)介绍的是一套旧版(legacy)语法:需开启 experimentalDecorators,装饰器函数签名为 (target, propertyKey, descriptor),且支持参数装饰器。TypeScript 5.0 起原生支持 TC39 Stage 3 标准装饰器,二者互不兼容,必须在 tsconfig 中二选一。

两套语法对比总览 ​

维度legacy(experimentalDecorators)TC39 标准(TS 5.0+,默认)
配置要求必须 experimentalDecorators: true无需任何配置
类装饰器签名(target: Function) => Function | void(value, context: ClassDecoratorContext) => Function | void
方法装饰器签名(target, propertyKey, descriptor)(value, context: ClassMethodDecoratorContext)
字段装饰器签名(target, propertyKey)(value, context: ClassFieldDecoratorContext)
访问器装饰器getter/setter 共用一个 descriptorgetter / setter 各自独立装饰
参数装饰器✅ 支持❌ 不支持
addInitializer❌ 无✅ context 上提供
context.kind / name / private / static❌ 无✅ 有
修改/替换成员直接改 descriptor.value通过返回值替换
提案状态TypeScript 独有实验性扩展TC39 Stage 3(走向 ECMAScript 标准)

标准装饰器的核心机制 ​

1. 统一签名为 (value, context)

标准装饰器的第一个参数是被装饰的值本身(方法、getter/setter、字段初值、类),第二个参数是 context 上下文对象,其中 kind 标明被装饰目标的种类:

context.kind含义value 是什么
'class'类装饰器类本身
'method'方法装饰器方法函数
'getter'getter 装饰器getter 函数
'setter'setter 装饰器setter 函数
'field'字段装饰器undefined(初值通过返回值注入)
'accessor'auto-accessor 装饰器一个 { get, set } 对象

context 还提供 name(成员名)、static(是否静态成员)、private(是否私有成员)、以及 addInitializer(cb)——注册一个在类定义完成/实例化后执行的初始化回调。它是标准装饰器替代 legacy 中"改 descriptor + 构造器注入"的关键能力。

2. 用返回值替换成员(而非改 descriptor)

标准装饰器不暴露 descriptor,而是通过返回值替换成员:方法/访问器装饰器返回新函数即替换,字段装饰器返回函数即作为字段初始化器,类装饰器返回新类即替换类。

typescript
// 官方 5.0 示例:loggedMethod —— 记录方法进出(无需改 descriptor)
function loggedMethod(
  originalMethod: any,
  context: ClassMethodDecoratorContext
) {
  const methodName = String(context.name);
  return function (this: any, ...args: any[]) {
    console.log(`LOG: Entering method '${methodName}'.`);
    const result = originalMethod.call(this, ...args);
    console.log(`LOG: Exiting method '${methodName}'.`);
    return result;
  };
}

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

  @loggedMethod
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

3. addInitializer 替代构造器注入

legacy 中"给实例自动绑定 this"需要改构造器或 descriptor;标准装饰器用 addInitializer:

typescript
function bound(_: any, context: ClassMethodDecoratorContext) {
  const methodName = context.name;
  if (context.private) {
    throw new Error(`'bound' cannot decorate private properties like ${String(methodName)}.`);
  }
  context.addInitializer(function (this: any) {
    // this 指向实例;把方法绑定到实例,等价于箭头函数字段
    this[methodName] = this[methodName].bind(this);
  });
}

class Person {
  constructor(public name: string) {}

  @bound
  greet() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

4. auto-accessor(自动访问器)

标准装饰器新增 accessor 关键字声明自动访问器,编译器自动生成私有字段 + getter/setter,配合 ClassAccessorDecoratorContext 装饰:

typescript
function loggedAccessor(
  target: { get: () => number; set: (v: number) => void },
  context: ClassAccessorDecoratorContext
) {
  const { get, set } = target;
  return {
    get() {
      console.log(`Reading ${String(context.name)}`);
      return get.call(this);
    },
    set(value: number) {
      console.log(`Writing ${String(context.name)} = ${value}`);
      set.call(this, value);
    },
  };
}

class Weather {
  @loggedAccessor
  accessor temp = 25; // 编译器生成私有字段 + get/set
}

如何选择 ​

  • 💡 新项目:直接使用 TC39 标准装饰器,无需配置,且是未来 ECMAScript 标准方向。
  • ⚠️ 依赖参数装饰器(如某些老版 ORM / DI 框架、Angular 风格的参数注入):标准装饰器不支持参数装饰器,只能继续用 legacy + experimentalDecorators。
  • ⚠️ 两套语法不能混用:开启 experimentalDecorators 后,@ 一律按 legacy 语义解析;关闭后按标准语义解析。迁移时需整体切换,不能逐文件渐进。

20. TypeScript 配置 ​

定义 ​

tsconfig.json 是 TypeScript 项目的配置文件,它告诉编译器如何编译 TypeScript 代码。

基础 tsconfig.json ​

json
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "react-jsx",
    "declaration": true,
    "sourceMap": true,
    "outDir": "./dist",
    "rootDir": "./src"
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist"]
}

核心配置详解 ​

配置项定义推荐值说明
target编译输出的 JS 版本ES2020越高则输出的 JS 越现代,polyfill 需求越少
module模块系统规范ESNext用打包工具时用 ESNext,Node.js 用 NodeNext
strict开启所有严格检查true包含 strictNullChecks、noImplicitAny 等
esModuleInteropESM 与 CommonJS 互操作true允许 import CommonJS 模块
skipLibCheck跳过声明文件类型检查true减少编译时间
isolatedModules每个文件独立编译true确保与打包工具(Babel、esbuild)兼容

注意事项 ​

strict: true 应该始终开启。关闭 strict 会禁用 strictNullChecks、 noImplicitAny 等多个关键检查,失去了使用 TypeScript 的大部分意义。从 JS 迁移时可以暂时关闭,但应有计划地逐步开启。

outDir 和 rootDir 要配套使用:如果不设置 rootDir,编译器可能根据文件目录结构自动选择根目录,导致 outDir 中的目录结构不符合预期。

路径别名的局限:paths 配置只在 TypeScript 编译阶段起作用。运行时需要打包工具或 Node.js 的路径解析支持。

include 和 exclude 是 glob 模式(如 src/**/*),不是正则表达式。


21. 实际项目最佳实践 ​

项目结构建议 ​

推荐的分层项目结构:

src/
  ├── types/          # 全局类型定义
  ├── utils/          # 工具函数
  ├── services/       # API 服务层
  ├── components/     # UI 组件
  ├── hooks/          # 自定义 Hooks
  ├── stores/         # 状态管理
  ├── pages/          # 页面组件
  └── index.ts        # 入口文件

类型策略 ​

  1. 优先使用接口(Interface)定义公开 API 的类型:接口更易扩展,错误信息更清晰。
  2. 使用类型别名(Type)组合复杂类型:联合类型、交叉类型、工具类型组合时用 type。
  3. 导出的类型都要有明确的命名:使用 PascalCase,加前缀如 I、T 需团队约定一致。
  4. 避免过度抽象:类型是为开发者服务的,不是负担。

核心最佳实践总结 ​

  1. 始终开启 strict: true。
  2. 多用 unknown,少用 any。
  3. 函数返回值类型显式标注,尤其是复杂逻辑。
  4. 使用可辨识联合处理不同形状的数据。
  5. 合理使用泛型约束,提高代码复用性。
  6. 优先使用 interface 而非 type 定义对象形状。
  7. 为第三方库编写或维护声明文件。

推荐学习路径 ​

  1. 入门:官方 Handbook — Basic Types → Interfaces → Functions
  2. 进阶:Generics → Type Manipulation → Utility Types
  3. 深入:Conditional Types → Mapped Types → Template Literal Types
  4. 实战:在真实项目中应用,阅读知名库的源码类型定义

推荐资源 ​

3.6 symbol 与 bigint ​

symbol ​

symbol 是 ES2015 引入的原始类型,表示唯一标识符:

typescript
const sym1 = Symbol('key');
const sym2 = Symbol('key');
console.log(sym1 === sym2); // false

// unique symbol —— 必须用 const 声明
const sym3: unique symbol = Symbol('unique');

bigint ​

bigint 用于表示任意精度的整数:

typescript
const big: bigint = 100n;
const result = big + 200n;

bigint 和 number 不能互相赋值,也不能一起运算。

3.7 object 类型 ​

object 表示非原始类型(非 number、string、boolean、symbol、null、undefined):

typescript
function acceptObject(obj: object) {
  console.log(obj);
}

acceptObject({ name: 'TS' }); // 合法
acceptObject(42);             // ❌ 错误

不要混淆 object(非原始类型) 与 {}(非 null/undefined 的任何类型) 以及 Object(所有类型的父类型)。

3.8 类型使用红线(来自官方 Do's and Don'ts) ​

应该做不应该做
使用联合类型使用 any
使用类型守卫缩小类型使用类型断言逃避检查
使用可选属性大量使用
使用泛型约束使用 any 绕过类型检查

注意事项 ​

类型系统的目标是减少运行时错误,而不是消灭所有类型标注。平衡类型安全和开发效率才是关键。

上下文类型(Contextual Typing) ​

TypeScript 能根据上下文推断类型,无需显式标注:

typescript
// 根据事件类型推断参数类型
document.addEventListener('click', function (event) {
  // event 被推断为 MouseEvent
});

新鲜度(Freshness / 严格对象字面量检查) ​

当将对象字面量赋值给一个类型时,TypeScript 会进行更严格的检查,不允许多余的属性:

typescript
interface Person { name: string; }

// 错误:对象字面量只能指定已知属性
const p: Person = { name: 'Alice', age: 25 }; // ❌

这种检查只在对象字面量上生效,变量赋值时不触发。

回调函数类型最佳实践 ​

回调的返回值类型 ​

typescript
// 好的做法:显式标注回调返回值
function forEach<T>(arr: T[], cb: (item: T, index: number) => void) {}

回调中的可选参数 ​

typescript
// 回调的可选参数不会导致类型不匹配
[1, 2, 3].forEach((item) => console.log(item)); // 合法

重载与回调 ​

使用重载而非可选参数区分不同的回调签名。

this 参数 ​

TypeScript 可以在函数中显式声明 this 的类型:

typescript
interface User {
  name: string;
}

function greet(this: User) {
  console.log(`Hello, ${this.name}`);
}

抽象类(Abstract Classes) ​

typescript
abstract class Animal {
  abstract makeSound(): void;
  move(): void {
    console.log('Moving...');
  }
}

class Dog extends Animal {
  makeSound() { console.log('Woof!'); }
}

参数属性(Parameter Properties) ​

在构造器中直接用访问修饰符声明属性:

typescript
class Person {
  constructor(
    public name: string,
    private age: number,
    readonly id: string
  ) {}
}

Mixin 模式 ​

typescript
type Constructor<T = {}> = new (...args: any[]) => T;

function Timestamped<TBase extends Constructor>(Base: TBase) {
  return class extends Base {
    timestamp = new Date();
  };
}

in 操作符缩小 ​

typescript
type Fish = { swim: () => void };
type Bird = { fly: () => void };

function move(animal: Fish | Bird) {
  if ('swim' in animal) {
    animal.swim(); // Fish
  } else {
    animal.fly();  // Bird
  }
}

instanceof 缩小 ​

typescript
class Cat { meow() {} }
class Dog { bark() {} }

function speak(animal: Cat | Dog) {
  if (animal instanceof Cat) {
    animal.meow();
  }
}

赋值缩小 ​

TypeScript 会根据赋值缩小变量类型:

typescript
let value: string | number = 'hello';
value = 42; // 此时 value 类型被缩小为 number

闭包中的类型缩小(TypeScript 5.4+) ​

TypeScript 5.4 改进了闭包中的类型缩小:

typescript
function fn(x: string | number) {
  if (typeof x === 'string') {
    setTimeout(() => {
      x.toUpperCase(); // TS 5.4+ 能正确缩小
    });
  }
}

15.6 索引访问类型(Indexed Access Types) ​

typescript
type Person = { name: string; age: number };
type NameType = Person['name']; // string

15.7 分布式条件类型(Distributive Conditional Types) ​

typescript
type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>; // string[] | number[]

15.8 as const 的深层原理 ​

typescript
const arr = [1, 2, 3] as const;
// arr 的类型是 readonly [1, 2, 3]

NoInfer<T>(TypeScript 5.4+) ​

typescript
function create<T>(c: { new(): T }, ..._: NoInfer<T>[]): T;

工具类型的实现原理 ​

typescript
// Partial<T> 的源码实现
type MyPartial<T> = {
  [P in keyof T]?: T[P];
};

strict 家族详解 ​

strict: true 是一个"全家桶"开关,开启它等于同时开启下面 8 个子选项:

子选项说明属于 strict?
strictNullChecksnull/undefined 不能赋值给其他类型✅
noImplicitAny禁止隐式 any✅
strictFunctionTypes函数类型参数逆变检查✅
strictBindCallApply检查 bind/call/apply 的参数✅
strictPropertyInitialization类属性必须初始化(依赖 strictNullChecks)✅
noImplicitThis禁止隐式 any 的 this✅
useUnknownInCatchVariablescatch 变量默认 unknown✅
alwaysStrict输出 "use strict"✅

⚠️ 容易误会的几个选项:以下选项不属于 strict: true,需要单独显式开启——

  • noImplicitReturns:函数所有代码路径必须有返回值(不影响 void 函数)
  • noFallthroughCasesInSwitch:switch 中 case 不允许穿透(fall-through)
  • noUnusedLocals / noUnusedParameters:未使用的局部变量 / 参数报错
  • noImplicitOverride:覆写父类成员必须写 override 关键字
  • exactOptionalPropertyTypes:可选属性不能显式赋 undefined

因此,团队若想获得"比 strict 更严格"的检查,应在 strict: true 基础上再手动追加这几个选项,而不是误以为 strict: true 已经包含它们。

moduleResolution 策略对比 ​

策略说明适用场景
classicTS 1.6 之前的解析方式❌ 不推荐
node(现称 node10)Node.js 旧版 CommonJS require 解析Node < 10 老项目
node16 / nodenext现代 Node.js:按输出中的 import/require 分别走 ESM / CJS 算法Node.js 新项目
bundler类似 nodenext,但相对导入不要求写扩展名前端项目(Vite/Webpack/esbuild)

💡 node16 vs nodenext:二者行为完全一致(node16 是 nodenext 的历史别名,nodenext 是当前推荐名),都要求 module 也设为 nodenext(或 node16)配合使用。

⚠️ nodenext 与 bundler 的关键差异:

  • 扩展名要求:nodenext 下 ESM 相对导入必须写全扩展名(import { a } from './a.js'),因为要严格对齐 Node.js 运行时行为;bundler 允许省略扩展名(from './a'),因为打包器负责补全。
  • package.json exports / imports:两者都支持;但 bundler 不区分 import / require 条件(假定交给打包器处理),而 nodenext 会根据文件是 ESM 还是 CJS 选择不同的条件分支。
  • 配套 module 值:bundler 配 "module": "esnext"(或 preserve);nodenext 配 "module": "nodenext"。
  • 是否产出:bundler 假定代码由打包器处理,通常只做类型检查(noEmit);nodenext 可直接产出交给 Node.js 运行的 JS。

22. TypeScript 常见陷阱与反模式 ​

22.1 过度使用 any ​

typescript
// 错误做法
function process(data: any) {
  return data.something;
}

// 正确做法
function process<T extends { something: unknown }>(data: T) {
  return data.something;
}

22.2 滥用类型断言(as) ​

typescript
// 错误做法
const user = JSON.parse(jsonStr) as User;

// 正确做法
function parseUser(data: string): User | null {
  try {
    const parsed = JSON.parse(data);
    // 运行时验证
    if (typeof parsed.name === 'string') return parsed;
  } catch {}
  return null;
}

22.3 忽略 strictNullChecks ​

关闭 strictNullChecks 会导致大量潜在的空指针异常。

22.4 用包装对象类型代替原始类型 ​

typescript
// 错误做法
function greet(name: String) { /* ... */ }

// 正确做法
function greet(name: string) { /* ... */ }

22.5 枚举数值隐式依赖 ​

typescript
enum Status {
  Active,   // 0
  Inactive, // 1
  Pending,  // 2
}

新增枚举成员会改变后续成员的隐式值,破坏已有数据。

22.6 不必要的泛型参数 ​

typescript
// 过度设计
function identity<T>(value: T): T { return value; }

// 简单场景直接用具体类型
function log(value: string): void { console.log(value); }

22.7 将可选参数用于回调 ​

typescript
// 错误做法
function fetch(cb: (err?: Error, data?: Data) => void) {}

// 正确做法:用重载或不同回调
function fetch(onSuccess: (data: Data) => void, onError: (err: Error) => void) {}

22.8 忘记 TypeScript 是编译时系统 ​

TypeScript 类型在运行时被完全擦除,不能用来做运行时验证。

22.10 总结:12 条常见陷阱速查表 ​

#陷阱解决方案
1滥用 any使用 unknown + 类型守卫
2过度使用类型断言用类型守卫代替
3忽略 strictNullChecks始终开启
4用包装类型代替原始类型用小写原始类型
5枚举数值隐式依赖显式赋值或用字符串枚举
6不必要的泛型只在需要时加泛型
7回调可选参数使用重载
8混淆编译时与运行时运行时做验证
9滥用 as const只在需要字面量类型时使用
10过度使用映射类型考虑工具类型替代方案
11忽略类型导出导出共用的类型定义
12不编写声明文件为公共 API 编写 .d.ts

23. TypeScript 版本演进与核心新特性 ​

定义 ​

TypeScript 每 3-5 个月发布一个次版本更新,持续引入新的类型特性和语言改进。了解版本演进有助于项目迁移和合理选用新特性。

4.x 系列核心新特性 ​

版本核心新特性
4.0可变元组类型、标签元组、类属性从构造函数推断
4.1模板字面量类型、映射类型的键重映射
4.2更智能的类型别名保留、元组中的 rest 元素
4.3override 关键字、静态索引签名
4.4控制流分析中的别名条件、catch 中的 unknown
4.5Awaited 类型、type 修饰符导入
4.6更强大的递归类型检查、索引访问类型改进
4.7泛型实例化表达式、模块检测
4.8改进的 infer 类型推断、约束修饰符
4.9satisfies 操作符、unused 检查改进

对项目最重要的特性 ​

  1. 模板字面量类型(4.1):动态构建字符串字面量类型
  2. 可变元组类型(4.0):更灵活的参数类型
  3. satisfies 操作符(4.9):类型检查 + 保留推导类型
  4. override 关键字(4.3):明确覆盖父类方法
  5. Awaited 类型(4.5):解包 Promise 类型

版本升级建议 ​

  1. 保持最新:建议升级到最新稳定版(当前为 7.x)
  2. 渐进升级:先升级 TypeScript 版本,再逐步启用新特性
  3. 配合工具:确保 ESLint、Webpack/Vite 等工具链与 TS 版本兼容

5.x 系列核心新特性 ​

版本核心新特性
5.0新版装饰器标准、const 类型参数、枚举增强
5.1未定义返回值函数的隐式 return 检查
5.2using 声明(显式资源管理)
5.3导入属性、缩小窄化改进
5.4NoInfer 类型、闭包中的类型缩小、分组导入
5.5推断类型谓词、隔离声明(Isolated Declarations)
5.6禁止有问题的 nullish/truthy 检查、迭代器帮助方法
5.7未初始化变量检查、--target es2024、--rewriteRelativeImportExtensions 路径重写
5.8--module node18、--erasableSyntaxOnly、return 表达式分支细化检查
5.9import defer、--module node20、可展开悬停(Expandable Hovers)

注:TypeScript 6.0(2026-03)是最后一个基于 JavaScript 实现的版本,作为向 7.0 迁移的过渡版本;TypeScript 7.0(2026-07)是基于 Go 的原生编译器(原 Project Corsa),编译与类型检查性能约提升 10 倍。

5.5 关键特性详解 ​

推断类型谓词(Inferred Type Predicates) ​

typescript
function isString(value: unknown) {
  return typeof value === 'string';
}
// TS 5.5 自动推断返回类型为 value is string

隔离声明(Isolated Declarations) ​

--isolatedDeclarations 选项确保每个文件的声明可以独立生成,不依赖跨文件类型推断。

5.6 关键特性详解 ​

禁止有问题的 Nullish/Truthy 检查 ​

TypeScript 5.6 对 == null、== undefined 的使用做了更严格的检查。

迭代器帮助方法(Iterator Helper Methods) ​

typescript
const iter = [1, 2, 3].values()
  .map(x => x * 2)
  .filter(x => x > 2);

24. 高级类型特殊写法与类型体操 ​

为什么前面章节没有重点讲这些写法 ​

前面的 keyof、条件类型、映射类型、工具类型章节主要讲的是单个语法点。你前面问到的这些写法属于更高一层:

  • 把多个语法点组合成类型工具。
  • 用类型系统模拟过滤、遍历、判断、提取、合并。
  • 为函数式 API、链式 API、框架类型、组件库类型提供精确推导。

所以它们看起来“特殊”,不是因为语法乱写,而是因为它们把 TypeScript 的类型系统当成一门小型类型编程语言来使用。

这一章重点解决三个问题:

  1. 这类写法到底在写什么。
  2. 为什么 TypeScript 可以这么写。
  3. 什么场景才值得这么写。

24.1 函数接口、调用签名与重载接口 ​

你之前问到的例子:

typescript
interface MapperFunc<I, O> {
  (): MapperFunc<I, O>;
  (input: I[]): O[];
}

interface MapFunc {
  (): MapFunc;
  <I, O>(mapper: (item: I) => O): MapperFunc<I, O>;
  <I, O>(mapper: (item: I) => O, input: I[]): O[];
}

这是 函数接口 + 调用签名重载 + 泛型 + 柯里化。

普通接口描述对象:

typescript
interface User {
  id: number;
  name: string;
}

函数接口描述“可以被调用的东西”:

typescript
interface Fn {
  (name: string): string;
}

const greet: Fn = (name) => `hello ${name}`;

如果接口里写多条调用签名,就是函数重载:

typescript
interface ToArray {
  (value: string): string[];
  (value: number): number[];
}

MapperFunc<I, O> 的含义:

typescript
interface MapperFunc<I, O> {
  (): MapperFunc<I, O>;
  (input: I[]): O[];
}

它表示一个函数有两种调用方式:

typescript
mapperFn();        // 返回自己这一类函数
mapperFn(input);   // 输入 I[],输出 O[]

MapFunc 的含义:

typescript
interface MapFunc {
  (): MapFunc;
  <I, O>(mapper: (item: I) => O): MapperFunc<I, O>;
  <I, O>(mapper: (item: I) => O, input: I[]): O[];
}

它表示 map 支持三种调用方式:

typescript
map();
map((item) => String(item));
map((item) => String(item), [1, 2, 3]);

完整示例:

typescript
declare const map: MapFunc;

const r1 = map((n: number) => String(n), [1, 2, 3]);
// string[]

const r2 = map((n: number) => String(n))([1, 2, 3]);
// string[]

const r3 = map()((n: number) => String(n))([1, 2, 3]);
// string[]

为什么这么写:

  • API 想同时支持普通调用和柯里化调用。
  • 每一步调用后都要保留泛型信息。
  • 只用一个普通函数类型无法表达多种调用方式。

常见场景:

  • 函数式工具库,例如 map、filter、reduce。
  • 链式 API。
  • 支持可选参数分步传入的工具函数。
  • 既是函数又有属性的对象。

函数也可以带属性:

typescript
interface RequestHandler {
  (req: Request, res: Response): void;
  routeName?: string;
}

这种接口描述的是:

一个函数,同时它身上还有额外属性。

注意:

  • 函数重载顺序会影响推导,通常把更具体的重载写前面。
  • 实现函数时要能兼容所有重载。
  • 过度设计重载会让错误信息很难读。

24.2 动态添加属性:交叉类型 + 映射类型 ​

你之前问到的例子:

typescript
type ObjectWithNewProp<T, K extends string, V> = T & { [NK in K]: V };

它的意思是:

在原对象类型 T 上,新增一个名为 K、类型为 V 的属性。

示例:

typescript
type User = {
  name: string;
};

type UserWithAge = ObjectWithNewProp<User, 'age', number>;

结果可以理解为:

typescript
type UserWithAge = {
  name: string;
} & {
  age: number;
};

拆开看:

typescript
T & ...

& 是交叉类型,表示同时拥有两边的结构。

typescript
{ [NK in K]: V }

这是映射类型,表示遍历 K 中的 key,并把每个 key 的值类型设为 V。

如果:

typescript
K = 'age'
V = number

那么:

typescript
{ [NK in K]: V }

就等价于:

typescript
{
  age: number;
}

为什么不能写成:

typescript
type Wrong<T, K extends string, V> = T & { K: V };

因为这样属性名是字面量 "K",不是泛型参数 K 代表的那个字符串。

等价常见写法:

typescript
type ObjectWithNewProp<T, K extends string, V> = T & Record<K, V>;

如果想支持 string | number | symbol:

typescript
type ObjectWithNewProp<T, K extends PropertyKey, V> = T & Record<K, V>;

PropertyKey 等价于:

typescript
string | number | symbol

封装函数:

typescript
type ObjectWithNewProp<T, K extends PropertyKey, V> = T & Record<K, V>;

function addProp<T, K extends PropertyKey, V>(
  obj: T,
  key: K,
  value: V,
): ObjectWithNewProp<T, K, V> {
  return {
    ...obj,
    [key]: value,
  } as ObjectWithNewProp<T, K, V>;
}

const user = addProp({ name: 'Alice' }, 'age', 18);

user.name; // string
user.age;  // number

同名属性冲突:

typescript
type User = {
  age: string;
};

type Bad = ObjectWithNewProp<User, 'age', number>;

Bad['age'] 会变成类似:

typescript
string & number

通常不可用。

如果你想“覆盖属性”,应该写:

typescript
type OverrideProp<T, K extends PropertyKey, V> =
  Omit<T, K> & Record<K, V>;

使用场景:

  • 给数据附加元信息,例如 requestId、createdAt。
  • 链式构建对象。
  • 插件系统给上下文对象扩展字段。
  • 表单状态给原始字段加校验结果。

24.3 映射类型修饰符:readonly、可选属性的增删 ​

映射类型不仅能遍历 key,还能修改属性修饰符。

typescript
type Mutable<T> = {
  -readonly [K in keyof T]: T[K];
};

含义:

  • keyof T:取出所有 key。
  • [K in keyof T]:遍历这些 key。
  • -readonly:移除 readonly。
  • T[K]:取当前 key 对应的值类型。

示例:

typescript
type User = {
  readonly id: number;
  readonly name: string;
};

type MutableUser = Mutable<User>;

结果:

typescript
type MutableUser = {
  id: number;
  name: string;
};

可选属性修饰符:

typescript
type RequiredByMapping<T> = {
  [K in keyof T]-?: T[K];
};

含义:

  • -?:移除可选属性的 ?。

添加修饰符:

typescript
type DeepReadonlyShallow<T> = {
  +readonly [K in keyof T]: T[K];
};

type PartialByMapping<T> = {
  [K in keyof T]+?: T[K];
};

常见修饰符:

  • readonly:添加 readonly。
  • -readonly:移除 readonly。
  • ?:添加可选。
  • -?:移除可选。
  • +readonly、+?:显式添加,通常 + 可以省略。

使用场景:

  • 把接口变成只读。
  • 把只读对象变成可编辑对象。
  • 表单草稿把所有字段变可选。
  • 提交数据前把所有字段变必填。

24.4 严格类型相等判断:ifEquals ​

你之前问到的例子:

typescript
type ifEquals<T, U, Y = unknown, N = never> =
  (<G>() => G extends T ? 1 : 2) extends
  (<G>() => G extends U ? 1 : 2)
    ? Y
    : N;

这个工具的作用是:

判断两个类型是否严格相等。相等返回 Y,不相等返回 N。

使用:

typescript
type A = ifEquals<string, string, 'yes', 'no'>;
// 'yes'

type B = ifEquals<string, number, 'yes', 'no'>;
// 'no'

为什么不直接写:

typescript
type SimpleEqual<T, U> = T extends U ? (U extends T ? true : false) : false;

因为这种写法在联合类型、any、泛型分发等情况下不够稳定。

高级写法:

typescript
(<G>() => G extends T ? 1 : 2) extends
(<G>() => G extends U ? 1 : 2)

可以理解为:

比较两个泛型函数在所有可能的 G 下表现是否一致。

这是一种常见类型体操技巧,用来更严格地比较类型。

更完整版本通常会双向比较:

typescript
type IfEquals<T, U, Y = true, N = false> =
  (<G>() => G extends T ? 1 : 2) extends
  (<G>() => G extends U ? 1 : 2)
    ? (<G>() => G extends U ? 1 : 2) extends
      (<G>() => G extends T ? 1 : 2)
      ? Y
      : N
    : N;

使用场景:

  • 判断某个属性是否 readonly。
  • 判断类型是否完全一致。
  • 写类型测试。
  • 实现高级工具类型。

注意:

  • 这种写法主要适合类型工具库。
  • 业务代码里不要到处写。
  • 遇到 any 时,类型相等判断可能仍然有边界情况。

24.5 提取 readonly key:GetReadonlyKeys ​

你之前问到的例子:

typescript
type ifEquals<T, U, Y = unknown, N = never> =
  (<G>() => G extends T ? 1 : 2) extends
  (<G>() => G extends U ? 1 : 2)
    ? Y
    : N;

type GetReadonlyKeys<T> = {
  [K in keyof T]-?: ifEquals<
    Pick<T, K>,
    { -readonly [P in K]: T[P] },
    never,
    K
  >
}[keyof T];

目标:

获取对象类型中所有 readonly 属性名。

示例:

typescript
type User = {
  readonly id: number;
  name: string;
  readonly createdAt?: Date;
};

type R = GetReadonlyKeys<User>;
// 'id' | 'createdAt'

分解过程:

typescript
[K in keyof T]-?

遍历所有 key,并移除可选标记,避免 ? 干扰 readonly 判断。

typescript
Pick<T, K>

取出原始属性。

如果:

typescript
type User = {
  readonly id: number;
};

那么:

typescript
Pick<User, 'id'>

得到:

typescript
{
  readonly id: number;
}

再看:

typescript
{ -readonly [P in K]: T[P] }

它构造一个“去掉 readonly 的当前属性”:

typescript
{
  id: number;
}

比较:

typescript
ifEquals<
  Pick<T, K>,
  { -readonly [P in K]: T[P] },
  never,
  K
>

逻辑:

  • 如果原属性和去掉 readonly 后一样,说明不是 readonly,返回 never。
  • 如果不一样,说明原来有 readonly,返回 K。

最后:

typescript
}[keyof T]

把映射对象的所有 value 取出来形成联合类型。

中间结果类似:

typescript
{
  id: 'id';
  name: never;
  createdAt: 'createdAt';
}

取值后:

typescript
'id' | never | 'createdAt'

never 会被消掉:

typescript
'id' | 'createdAt'

相关工具:

typescript
type GetMutableKeys<T> = {
  [K in keyof T]-?: ifEquals<
    Pick<T, K>,
    { -readonly [P in K]: T[P] },
    K,
    never
  >
}[keyof T];

使用场景:

  • 区分可编辑字段和只读字段。
  • 表单编辑权限。
  • DTO 类型生成。
  • 框架根据模型字段生成 API 约束。

24.6 联合类型过滤:LookUp、Extract、Exclude ​

你之前问到的例子:

typescript
type LookUp<U, T> = U extends { type: T } ? U : never;

作用:

从联合类型 U 中找出 type 属性等于 T 的成员。

示例:

typescript
type Cat = {
  type: 'cat';
  name: string;
  meow: () => void;
};

type Dog = {
  type: 'dog';
  name: string;
  bark: () => void;
};

type Animal = Cat | Dog;

type Result = LookUp<Animal, 'dog'>;
// Dog

为什么可以:

typescript
U extends { type: T } ? U : never

当 U 是联合类型,并且 U 是裸类型参数时,条件类型会自动分发。

也就是:

typescript
LookUp<Cat | Dog, 'dog'>

会变成:

typescript
(Cat extends { type: 'dog' } ? Cat : never)
|
(Dog extends { type: 'dog' } ? Dog : never)

结果:

typescript
never | Dog

never 在联合类型中会被消掉:

typescript
Dog

这就是 分布式条件类型。

等价内置工具:

typescript
type LookUp<U, T> = Extract<U, { type: T }>;

Extract 用来保留能匹配某个类型的联合成员。

Exclude 用来排除能匹配某个类型的联合成员:

typescript
type NotDog = Exclude<Animal, { type: 'dog' }>;
// Cat

使用场景:

  • 可辨识联合类型。
  • 事件系统按事件名取事件 payload。
  • API 状态类型按 status 提取结果。
  • Redux action、状态机、表单事件建模。

示例:

typescript
type ApiResult =
  | { type: 'success'; data: string[] }
  | { type: 'error'; message: string }
  | { type: 'loading' };

type SuccessResult = LookUp<ApiResult, 'success'>;
// { type: 'success'; data: string[] }

24.7 条件类型中的 infer:从类型中提取类型 ​

infer 只能出现在条件类型的 extends 分支中,用来声明一个待推断的类型变量。

提取数组元素:

typescript
type ElementType<T> = T extends Array<infer Item> ? Item : never;

type A = ElementType<string[]>;
// string

提取 Promise 结果:

typescript
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;

type A = UnwrapPromise<Promise<number>>;
// number

提取函数返回值:

typescript
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (id: number) => Promise<string>;

type R = MyReturnType<Fn>;
// Promise<string>

提取函数参数:

typescript
type MyParameters<T> = T extends (...args: infer P) => any ? P : never;

type Fn = (id: number, name: string) => void;

type P = MyParameters<Fn>;
// [id: number, name: string]

递归解包 Promise:

typescript
type DeepAwaited<T> = T extends Promise<infer R> ? DeepAwaited<R> : T;

type A = DeepAwaited<Promise<Promise<string>>>;
// string

使用场景:

  • 从函数中提取参数和返回值。
  • 从 Promise 中提取异步结果。
  • 从数组、元组、对象中提取内部类型。
  • 写框架类型适配器。

注意:

  • infer 不是运行时代码。
  • infer 只能在类型层使用。
  • 如果类型不匹配,通常返回 never 或原类型。

24.8 分布式条件类型与如何关闭分发 ​

分布式条件类型:

typescript
type ToArray<T> = T extends any ? T[] : never;

type A = ToArray<string | number>;
// string[] | number[]

为什么不是:

typescript
(string | number)[]

因为 T 是裸类型参数,传入联合类型时会分发。

关闭分发:

typescript
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;

type A = ToArrayNonDist<string | number>;
// (string | number)[]

关键:

typescript
[T] extends [any]

把 T 包在元组里,T 不再是裸类型参数,所以不会分发。

常见用途:

  • 需要逐个过滤联合成员时,使用分发。
  • 需要把联合类型当成整体判断时,关闭分发。

例子:

typescript
type IsString<T> = T extends string ? true : false;

type A = IsString<string | number>;
// true | false

type IsStringWhole<T> = [T] extends [string] ? true : false;

type B = IsStringWhole<string | number>;
// false

24.9 key remapping:映射类型中的 as ​

TypeScript 可以在映射类型里用 as 重命名 key。

typescript
type PrefixKeys<T> = {
  [K in keyof T as `api_${string & K}`]: T[K];
};

示例:

typescript
type User = {
  id: number;
  name: string;
};

type ApiUser = PrefixKeys<User>;

结果:

typescript
type ApiUser = {
  api_id: number;
  api_name: string;
};

过滤 key:

typescript
type RemoveKind<T> = {
  [K in keyof T as K extends 'kind' ? never : K]: T[K];
};

type Shape = {
  kind: 'circle';
  radius: number;
};

type WithoutKind = RemoveKind<Shape>;
// { radius: number }

为什么 never 可以过滤 key:

映射类型中,如果 key 被重映射成 never,这个属性就会被移除。

生成事件名:

typescript
type EventHandlers<T> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (value: T[K]) => void;
};

type Form = {
  name: string;
  age: number;
};

type Handlers = EventHandlers<Form>;

结果:

typescript
type Handlers = {
  onNameChange: (value: string) => void;
  onAgeChange: (value: number) => void;
};

涉及知识点:

  • 映射类型。
  • 模板字面量类型。
  • key remapping。
  • Capitalize 内置字符串工具类型。
  • string & K 把 key 收窄成字符串,避免 symbol key 干扰。

使用场景:

  • 根据后端字段生成前端事件名。
  • 根据模型字段生成 setter。
  • 过滤内部字段。
  • 自动生成 API 类型。

24.10 as const、satisfies 与 const 类型参数 ​

as const 用来保留字面量类型,并把对象变成深层 readonly。

typescript
const routes = {
  home: '/',
  user: '/users/:id',
} as const;

type RouteName = keyof typeof routes;
// 'home' | 'user'

type RoutePath = typeof routes[RouteName];
// '/' | '/users/:id'

如果不用 as const:

typescript
const routes = {
  home: '/',
  user: '/users/:id',
};

路径值通常会被推断成:

typescript
string

satisfies 用来做结构检查,同时保留更精确的推断。

typescript
const routes = {
  home: '/',
  user: '/users/:id',
} as const satisfies Record<string, string>;

含义:

  • as const:保留精确字面量。
  • satisfies Record<string, string>:检查它是否满足字符串字典结构。
  • 不会像直接类型注解那样把值全部放宽成 string。

对比:

typescript
const a: Record<string, string> = {
  home: '/',
};

type A = typeof a.home;
// string

const b = {
  home: '/',
} as const satisfies Record<string, string>;

type B = typeof b.home;
// '/'

const 类型参数:

typescript
function tuple<const T extends readonly unknown[]>(value: T): T {
  return value;
}

const t = tuple(['a', 'b']);
// readonly ['a', 'b']

如果不用 const 类型参数:

typescript
function tuple<T extends readonly unknown[]>(value: T): T {
  return value;
}

const t = tuple(['a', 'b']);
// string[]

使用场景:

  • 路由表。
  • 权限码表。
  • 状态机配置。
  • 表单字段配置。
  • 需要从值反推出精确类型的地方。

注意:

  • as const 会带来 readonly。
  • 如果后续要修改对象,不要随便 as const。
  • satisfies 是检查,不是类型断言。
  • as 是告诉编译器“相信我”,satisfies 是让编译器检查“是否满足”。

24.11 NoInfer:阻止错误位置参与泛型推断 ​

NoInfer<T> 用来阻止某个位置参与泛型推断。

常见问题:

typescript
function createState<T>(initial: T, fallback: T) {
  return initial ?? fallback;
}

createState('open', 'closed');

这里 T 可能被推断成:

typescript
'open' | 'closed'

如果你希望 T 只从第一个参数推断,第二个参数必须兼容第一个参数,可以写:

typescript
function createState<T>(initial: T, fallback: NoInfer<T>) {
  return initial ?? fallback;
}

createState('open', 'open');   // OK
createState('open', 'closed'); // Error

典型场景:

typescript
function createMachine<TState extends string>(
  states: readonly TState[],
  initial: NoInfer<TState>,
) {
  return { states, initial };
}

createMachine(['idle', 'loading', 'success'] as const, 'idle'); // OK
createMachine(['idle', 'loading', 'success'] as const, 'error'); // Error

为什么需要:

  • 泛型可能从多个参数位置一起推断。
  • 有些参数应该被前面的参数约束,而不是反过来扩大泛型。
  • NoInfer 能保住“来源参数”的权威性。

使用场景:

  • 状态机初始状态。
  • 默认值必须来自候选列表。
  • 表单字段名必须来自字段配置。
  • 路由名必须来自路由表。

24.12 品牌类型:Branded Types ​

TypeScript 是结构类型系统。两个类型结构一样,就容易互相赋值。

typescript
type UserId = string;
type OrderId = string;

const userId: UserId = 'u1';
const orderId: OrderId = userId; // 可以,但语义上可能错误

品牌类型给基础类型附加一个只在类型层存在的标记。

typescript
type Brand<T, B extends string> = T & { readonly __brand: B };

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

使用:

typescript
function makeUserId(value: string): UserId {
  return value as UserId;
}

const userId = makeUserId('u1');

function getUser(id: UserId) {}

getUser(userId);      // OK
getUser('u1');        // Error

为什么这么写:

  • 运行时仍然是 string。
  • 类型层多了一个不可见标记。
  • 避免不同业务含义的 string 混用。

使用场景:

  • 用户 ID。
  • 订单 ID。
  • 金额单位。
  • 已校验字符串。
  • 经过权限校验的 token。

注意:

  • 品牌类型需要通过工厂函数创建。
  • 不要到处 as UserId,否则品牌失去意义。
  • 品牌字段通常不会真的存在于运行时对象中。

24.13 UnionToIntersection:联合类型转交叉类型 ​

高级工具类型中经常见到:

typescript
type UnionToIntersection<U> =
  (U extends any ? (arg: U) => void : never) extends
  (arg: infer I) => void
    ? I
    : never;

示例:

typescript
type U = { name: string } | { age: number };

type I = UnionToIntersection<U>;
// { name: string } & { age: number }

为什么可以:

第一步利用分布式条件类型:

typescript
U extends any ? (arg: U) => void : never

如果 U 是联合类型:

typescript
{ name: string } | { age: number }

会变成:

typescript
((arg: { name: string }) => void)
|
((arg: { age: number }) => void)

第二步利用函数参数的逆变位置,把联合参数合并成交叉参数。

这个技巧比较抽象,主要用于类型库内部。

使用场景:

  • 合并多个插件上下文类型。
  • 合并多个对象片段。
  • 从联合函数中推导共同可接受参数。

注意:

  • 这是高级类型体操,不建议业务代码频繁使用。
  • 如果团队大部分人看不懂,应封装成语义清晰的工具类型。

24.14 元组递归与可变元组类型 ​

元组可以用于精确描述函数参数。

typescript
type Fn = (...args: [id: number, name: string]) => void;

提取第一个元素:

typescript
type First<T extends readonly unknown[]> =
  T extends readonly [infer F, ...unknown[]] ? F : never;

type A = First<[1, 2, 3]>;
// 1

提取剩余元素:

typescript
type Tail<T extends readonly unknown[]> =
  T extends readonly [unknown, ...infer R] ? R : [];

type A = Tail<[1, 2, 3]>;
// [2, 3]

递归反转元组:

typescript
type Reverse<T extends readonly unknown[]> =
  T extends readonly [infer F, ...infer R]
    ? [...Reverse<R>, F]
    : [];

type A = Reverse<[1, 2, 3]>;
// [3, 2, 1]

组合函数参数:

typescript
type PrependArg<F, A> =
  F extends (...args: infer P) => infer R
    ? (arg: A, ...args: P) => R
    : never;

type Fn = (name: string) => boolean;

type NewFn = PrependArg<Fn, number>;
// (arg: number, name: string) => boolean

使用场景:

  • 封装函数 wrapper。
  • 保留原函数参数。
  • 中间件组合。
  • curry、compose、pipe 类型。
  • CLI 参数类型建模。

24.15 Prettify / Expand:让复杂类型更易读 ​

交叉类型和映射类型组合后,IDE 中经常显示很难读:

typescript
type Raw = { name: string } & { age: number };

可以用 Prettify 展开:

typescript
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};

type User = Prettify<{ name: string } & { age: number }>;

IDE 中更容易看到:

typescript
{
  name: string;
  age: number;
}

注意:

  • Prettify 不改变运行时。
  • 它只是让类型显示更友好。
  • 常用于工具类型最终输出。

组合前面覆盖属性:

typescript
type Override<T, U> = Prettify<Omit<T, keyof U> & U>;

type User = {
  id: number;
  age: string;
};

type NewUser = Override<User, { age: number }>;
// { id: number; age: number }

24.16 类型体操常见知识点速查 ​

写法名称核心作用
<T>() => ...泛型函数类型用于严格类型比较、延迟推断
T extends U ? X : Y条件类型根据类型关系返回不同类型
T extends infer R ? ...infer 推断从已有类型结构中提取局部类型
T extends any ? ... : never分布式条件类型遍历联合类型成员
[T] extends [U] ? ... : ...关闭分发把联合类型当整体判断
[K in keyof T]映射类型遍历对象 key
[K in keyof T as NewKey]key remapping重命名或过滤 key
-readonly移除只读把 readonly 属性变可写
-?移除可选把可选属性变必填
T[K]索引访问类型取对象某个 key 的值类型
keyof Tkey 联合取对象所有属性名
never不可能类型在联合类型中过滤成员
as constconst 断言保留字面量类型并深层 readonly
satisfies类型满足检查校验结构同时保留精确推断
NoInfer<T>阻止推断防止某个位置反向扩大泛型

24.17 什么时候应该写高级类型,什么时候不该写 ​

适合写高级类型的情况:

  • 你在写公共工具库。
  • 你在写框架、组件库、SDK。
  • 你需要从配置对象自动推导 API 类型。
  • 你需要维护一组强关联类型,手写会产生重复和漂移。
  • 你需要让错误在编译期暴露,而不是运行期暴露。

不适合写高级类型的情况:

  • 普通业务类型,手写更清晰。
  • 类型工具比业务逻辑还难懂。
  • 团队维护成本明显上升。
  • 只是为了炫技。
  • 类型推导错误信息变得无法理解。

经验原则:

  1. 优先使用内置工具类型,例如 Pick、Omit、Extract、Exclude、Record。
  2. 再考虑组合内置工具。
  3. 最后才手写复杂类型体操。
  4. 复杂类型必须有注释和示例。
  5. 公共类型工具要配类型测试。

类型测试示例:

typescript
type Expect<T extends true> = T;

type Equal<T, U> =
  (<G>() => G extends T ? 1 : 2) extends
  (<G>() => G extends U ? 1 : 2)
    ? true
    : false;

type Test = Expect<Equal<LookUp<Animal, 'dog'>, Dog>>;

如果 LookUp<Animal, 'dog'> 不是 Dog,Test 就会报错。


24.18 本章重点回顾 ​

本章提到的特殊写法,本质上都围绕几件事:

  • 用条件类型做判断。
  • 用分布式条件类型过滤联合类型。
  • 用 never 删除不需要的成员。
  • 用映射类型遍历对象属性。
  • 用映射修饰符增删 readonly 和 ?。
  • 用 infer 从复杂类型中提取局部类型。
  • 用模板字面量和 key remapping 生成新 key。
  • 用调用签名重载描述多种函数调用方式。
  • 用 as const、satisfies、NoInfer 控制推断精度。

简单总结:

TypeScript 高级类型不是为了把代码写复杂,而是为了让“值之间已经存在的约束关系”在类型层也表达出来。

如果类型表达式不能让调用方更安全、更清晰、更少重复,它就不值得写得很复杂。

基于 VitePress 构建的个人学习文档知识库