Skip to content
Gains Summary
Main Navigation 首页 / Home
C++ 编程 / C++ Programming
系统与高性能 / Systems & Performance
Web 开发 / Web Development
人工智能 / Artificial Intelligence
工业软件 / Industrial Software
其他内容 / Other Topics
C++ 编程 / C++系统与性能 / SystemsWeb 开发 / Web人工智能 / AI工业软件 / Industrial

外观

Sidebar Navigation

← Web 开发 / Web Development

JavaScript 与 TypeScript / JavaScript & TypeScript

1. JavaScript 与 TypeScript 学习路线 / JavaScript and TypeScript Learning Path

2. JavaScript 核心语义:值、对象、函数与模块 / JavaScript Core Semantics: Values, Objects, Functions, and Modules

3. TypeScript 类型系统:从结构类型到类型编程 / The TypeScript Type System from Structural Typing to Type-Level Programming

4. JavaScript 异步编程与事件循环 / Asynchronous JavaScript and the Event Loop

本页目录

TypeScript 类型系统:从结构类型到类型编程 / The TypeScript Type System from Structural Typing to Type-Level Programming ​

📅 创建时间:2026-07-28 🏷️ 标签:#TypeScript #TypeSystem #泛型 #类型编程 📚 前置知识:[[01-javascript-runtime-and-language]]


📋 本章目标 ​

  • 理解 TypeScript 结构类型系统的设计哲学,以及与名义类型的本质区别
  • 掌握基本类型、字面量类型、const 推断和类型收窄的完整工具箱
  • 理解 unknown / any / never 的语义差异和选择决策树
  • 能够区分 type 和 interface 的使用场景,包括声明合并
  • 掌握泛型约束、默认值、"泛型只用一次"启发式与实战模式
  • 理解 keyof、typeof、索引访问、映射类型、条件类型和 infer 构成类型编程体系
  • 能够使用 as const、satisfies、模板字面量类型和工具类型表达精确约束

第1部分:结构类型 —— TypeScript 的哲学基石 ​

1.1 什么是结构类型 ​

TypeScript 采用结构类型系统(Structural Type System):类型兼容性由值的形状决定,而非声明的名称或继承链。

┌─────────────────────────────────────────────────────────────┐
│              结构类型 vs 名义类型                             │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  名义类型(Nominal)—— Java / C#                            │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  class Dog { bark() {} }                            │   │
│  │  class Cat { meow() {} }                            │   │
│  │  let d: Dog = new Cat()  // ❌ 类型不兼容            │   │
│  │  // 即使结构完全相同,名称不同就不可赋值               │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  结构类型(Structural)—— TypeScript                        │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  interface Dog { name: string; bark(): void }       │   │
│  │  interface Cat { name: string; bark(): void }       │   │
│  │  let d: Dog = { name: "x", bark(){} } as Cat // ✅  │   │
│  │  // 形状兼容即可赋值,名称无关                        │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

这个设计源于 JavaScript 生态的现实:JSON 数据、第三方库对象、网络响应都没有类型标签。TypeScript 必须接受"任何形状匹配的对象"而不是"来自特定类的实例"。

1.2 鸭子类型与结构类型的区别 ​

┌─────────────────────────────────────────────────────────────┐
│              鸭子类型 → 结构类型的演进                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  鸭子类型(运行时):                                       │
│    "走起来像鸭子,叫起来像鸭子,那就是鸭子"                   │
│    → 运行时检查属性存在性,没有类型声明                      │
│                                                             │
│  结构类型(编译时):                                       │
│    "编译器在编译时检查形状是否兼容"                          │
│    → 类型安全 + 鸭子类型的灵活性                            │
│                                                             │
│  TypeScript = 结构类型 = 编译时的鸭子类型                   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

结构类型的实际含义:函数参数只需要声明它真正用到的字段,而非完整的对象类型。

ts
// 不要这样写——过度约束
function printUser(user: { id: string; name: string; email: string;
  avatar: string; createdAt: Date; updatedAt: Date }) {
  console.log(user.name)
}

// 这样写——最小约束,最大兼容
function printUser(user: { name: string }) {
  console.log(user.name)
}
1
2
3
4
5
6
7
8
9
10

第2部分:基础类型、字面量类型与推断 ​

2.1 基本类型与 let / const 推断 ​

TypeScript 对 let 和 const 采用不同的推断策略:

┌─────────────────────────────────────────────────────────────┐
│              let vs const 推断策略                           │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  let x = "hello"     →  x: string    (宽类型,可重新赋值)  │
│  const x = "hello"   →  x: "hello"   (字面量类型,不可变)  │
│                                                             │
│  let n = 42          →  n: number    (宽类型)              │
│  const n = 42        →  n: 42        (字面量类型)          │
│                                                             │
│  let arr = [1,2,3]   →  arr: number[]                       │
│  const arr = [1,2,3] →  arr: number[] (注意:元素仍可改)   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
ts
// const 声明的对象:属性类型被拓宽,但引用不可变
const config = {
  host: "localhost",   // string(不是 "localhost")
  port: 8080,          // number(不是 8080)
}

// 需要字面量类型时,使用 as const
const config2 = {
  host: "localhost",
  port: 8080,
} as const
// config2: { readonly host: "localhost"; readonly port: 8080 }
1
2
3
4
5
6
7
8
9
10
11
12

2.2 数组、元组与对象类型 ​

ts
// 数组:同类型元素
const names: string[] = ["Ada", "Grace"]

// 元组:固定长度、各位置独立类型
type Pair = [string, number]
const entry: Pair = ["age", 30]

// 可变元组(rest elements)
type CSVLine = [string, ...number[]]
const row: CSVLine = ["2026-07", 100, 200, 300]

// 对象:显式声明优于隐式推断(在函数边界)
interface User {
  id: string
  name: string
  email?: string            // 可选属性
  readonly createdAt: Date  // 只读属性
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

2.3 优先让编译器推断局部变量 ​

函数参数、公共返回值、模块边界应显式书写类型。局部变量优先推断:

ts
// ✅ 让编译器推断局部变量
const retries = 3        // number
const names = ["Ada"]    // string[]

// ✅ 函数边界显式声明
function findUser(id: string): Promise<User | undefined> {
  // 内部逻辑可以推断
  const query = `SELECT * FROM users WHERE id = ${id}`
  return db.query(query)
}
1
2
3
4
5
6
7
8
9
10

第3部分:联合类型、判别联合与穷尽检查 ​

3.1 联合类型的基本概念 ​

联合类型表示"多个类型中的某一个",是 TypeScript 最强大的特性之一。

┌─────────────────────────────────────────────────────────────┐
│              联合类型的本质                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  type A = string | number                                   │
│                                                             │
│        ┌─────────┐                                          │
│        │  string │──────┐                                   │
│        └─────────┘      │    ┌───────────────────┐         │
│                          ├───→│  string | number  │         │
│        ┌─────────┐      │    └───────────────────┘         │
│        │  number │──────┘                                   │
│        └─────────┘                                          │
│                                                             │
│  未收窄前,只能访问各成员的共有属性                          │
│  收窄后,可访问该分支的全部属性                              │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

3.2 判别联合(Discriminated Unions) ​

判别联合是 TypeScript 中最优雅的状态建模方式:用一个字面量字段区分变体。

ts
// 用 status 字段作为判别属性
type RequestState<T> =
  | { status: "idle" }
  | { status: "loading"; progress?: number }
  | { status: "success"; data: T }
  | { status: "error"; message: string; code?: number }

function render<T>(state: RequestState<T>): string {
  switch (state.status) {
    case "idle":
      return "等待中"
    case "loading":
      // state.progress 可访问
      return `加载中... ${state.progress ?? 0}%`
    case "success":
      // state.data 可访问
      return `成功:${JSON.stringify(state.data)}`
    case "error":
      // state.message 和 state.code 可访问
      return `错误 [${state.code ?? "未知"}]: ${state.message}`
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
┌─────────────────────────────────────────────────────────────┐
│              判别联合的设计要点                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 每个变体共享一个判别字段(discriminant)                 │
│     → 必须是字面量类型:"idle" | "loading" | ...           │
│                                                             │
│  2. 判别字段的值在整个联合类型中互不相交                     │
│     → 不能有两个变体都有 status: "loading"                  │
│                                                             │
│  3. switch 语句收窄到各分支后,TS 自动推断其他字段           │
│     → 无需手动类型断言                                      │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

3.3 用 never 实现穷尽检查 ​

ts
function assertNever(x: never): never {
  throw new Error(`Unexpected value: ${x}`)
}

function render<T>(state: RequestState<T>): string {
  switch (state.status) {
    case "idle":    return "等待中"
    case "loading": return "加载中..."
    case "success": return "成功"
    case "error":   return "错误"
    default:
      // 如果后续有人加了新变体但没更新 switch,这里会编译报错
      return assertNever(state)
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

assertNever 利用了 never 类型:如果把非 never 的值传给参数类型为 never 的函数,TypeScript 会报错。这是一种编译时的安全网。


第4部分:unknown / any / never 三剑客 ​

4.1 三种顶层/底层类型 ​

┌─────────────────────────────────────────────────────────────┐
│              TypeScript 类型层级(简化)                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                      unknown                                │
│                      ┌─────┐                                │
│                      │ any │  ← 不在正常层级中,是"逃逸"    │
│                      └─────┘                                │
│                         │                                   │
│              ┌──────────┼──────────┐                        │
│              │          │          │                        │
│           string     number     object                      │
│              │          │          │                        │
│              └──────────┼──────────┘                        │
│                         │                                   │
│                  string | number                            │
│                  (联合类型)                                │
│                         │                                   │
│                      never                                 │
│                      ┌─────┐                                │
│                      │never│  ← 底层类型,是所有人的子类型  │
│                      └─────┘                                │
│                                                             │
│  unknown = 顶层类型(所有类型都可赋值给它)                  │
│  never   = 底层类型(它可以赋值给任何类型)                  │
│  any     = 既像顶层又像底层(关闭类型检查)                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

4.2 选择决策树 ​

┌─────────────────────────────────────────────────────────────┐
│              选择哪个类型的决策树                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  这个值的类型我:                                           │
│                                                             │
│  完全确定 → 使用具体类型                                    │
│     例:function add(a: number, b: number): number          │
│                                                             │
│  不确定但需要先接收下来 → 使用 unknown                      │
│     例:function parse(input: unknown): Result              │
│     → 然后收窄(typeof / instanceof / schema 验证)        │
│                                                             │
│  理论上不可达 → 使用 never                                  │
│     例:抛出异常、死循环、穷尽检查                          │
│                                                             │
│  any → 尽量不要用                                           │
│     → 迁移遗留代码时的临时方案                              │
│     → 泛型约束无法满足时,unknown 几乎总比 any 更好         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

4.3 unknown 的收窄模式 ​

ts
function processValue(value: unknown): string {
  // 方式1:typeof 收窄
  if (typeof value === "string") {
    return value.toUpperCase()  // value: string
  }

  // 方式2:instanceof 收窄
  if (value instanceof Date) {
    return value.toISOString()  // value: Date
  }

  // 方式3:自定义类型守卫
  if (isUser(value)) {
    return value.name            // value: User
  }

  // 方式4:in 操作符
  if (
    typeof value === "object" &&
    value !== null &&
    "name" in value &&
    typeof (value as Record<string, unknown>).name === "string"
  ) {
    return (value as { name: string }).name
  }

  throw new Error("Unsupported value type")
}

function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value
  )
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

第5部分:type vs interface —— 选型的艺术 ​

5.1 核心差异 ​

┌─────────────────────────────────────────────────────────────┐
│              type vs interface 能力对比                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  能力                      type        interface            │
│  ───────────────────────────────────────────────────────    │
│  描述对象形状               ✅           ✅                 │
│  描述联合类型               ✅           ❌                 │
│  描述元组                   ✅           ✅(受限)          │
│  声明合并                   ❌           ✅                 │
│  extends / 交叉             ✅ (&)       ✅ (extends)        │
│  映射类型                   ✅           ❌                 │
│  条件类型                   ✅           ❌                 │
│  工具类型                   ✅           ❌                 │
│  错误信息可读性             一般         ✅(更好)          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

5.2 声明合并(Declaration Merging) ​

这是 interface 独有的能力,适用于需要扩展第三方类型或分模块声明接口的场景:

ts
// 分两次声明同一个 interface,TS 会自动合并
interface Window {
  title: string
}

interface Window {
  ts: typeof import("typescript")
}

// 结果等价于:
// interface Window {
//   title: string
//   ts: typeof import("typescript")
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14

type 重复声明会报错。声明合并是 interface 最关键的差异化能力。

5.3 何时用哪个 ​

┌─────────────────────────────────────────────────────────────┐
│              选型建议                                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  优先使用 interface,当:                                   │
│  • 定义对象的形状(尤其是公共 API、组件 props)             │
│  • 需要声明合并(扩展第三方库、模块增强)                   │
│  • 希望获得更好的错误提示(interface 展开后错误更清晰)     │
│                                                             │
│  必须使用 type,当:                                        │
│  • 需要联合类型、交叉类型                                   │
│  • 需要映射类型、条件类型                                   │
│  • 需要元组(虽然 interface 也可以,但 type 更直观)        │
│  • 需要工具类型(Partial、Pick 等基于 type 的操作)         │
│                                                             │
│  通用原则:                                                 │
│  → 定义对象形状用 interface,类型运算用 type                │
│  → 同一个项目中保持一致                                     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
ts
// ✅ 对象形状 → interface
interface User {
  id: string
  name: string
  email: string
}

// ✅ 类型运算 → type
type UserSummary = Pick<User, "id" | "name">
type UserOrError = User | { error: string }

// ✅ 函数的形状也可以用 type(更紧凑)
type GetUser = (id: string) => Promise<User>

// 但 interface 也可以表示函数
interface GetUserFn {
  (id: string): Promise<User>
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

第6部分:泛型 —— 表达关系而非减少字符 ​

6.1 泛型的本质 ​

泛型的价值不是写更少的代码,而是表达输入和输出之间的类型关系。

┌─────────────────────────────────────────────────────────────┐
│              泛型表达的是"关系"                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  不用泛型:                                                 │
│  function first(arr: any[]): any { return arr[0] }          │
│  → 输入和输出之间没有类型关联                               │
│                                                             │
│  用泛型:                                                   │
│  function first<T>(arr: T[]): T | undefined {               │
│    return arr[0]                                            │
│  }                                                          │
│  → "输出类型 = 数组元素类型" 这个关系被精确表达             │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

6.2 泛型约束 ​

约束应尽可能小 —— 只声明泛型"真正需要"的能力。

ts
// ❌ 过度约束
function sortBy<T extends { id: string; name: string; createdAt: Date }>(
  items: T[]
): T[] { /* ... */ }

// ✅ 最小约束 —— 只声明真正需要的
function byId<T extends { id: string }>(items: readonly T[]) {
  return new Map(items.map(item => [item.id, item]))
}

// ✅ 不需要任何约束时,不写 extends
function identity<T>(value: T): T {
  return value
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

6.3 泛型默认值 ​

ts
// 为泛型参数提供默认类型
interface ApiResponse<T = unknown> {
  success: boolean
  data: T
  message: string
}

// 使用默认值:T 推定为 unknown
const r1: ApiResponse = { success: true, data: null, message: "ok" }

// 显式指定类型
const r2: ApiResponse<User> = {
  success: true,
  data: { id: "1", name: "Ada" },
  message: "ok",
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

6.4 "泛型只用一次"启发式 ​

┌─────────────────────────────────────────────────────────────┐
│              "泛型只用一次" 检查清单                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  如果泛型参数 T 在函数签名中只出现一次:                     │
│                                                             │
│  ❌ function foo<T extends string>(x: T): void              │
│     → T 只出现在参数,与其他参数/返回值无关联               │
│     → 泛型没有表达任何关系                                  │
│     → 直接用 string 即可                                   │
│                                                             │
│  ✅ function first<T>(arr: T[]): T | undefined              │
│     → T 出现在参数和返回值,表达了关系                      │
│                                                             │
│  ⚠️ 例外:函数重载、高阶函数、类型构建器                    │
│     function identity<T>(x: T): T   ← T 出现两次,成立     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

6.5 实战泛型模式 ​

ts
// 模式1:约束链 —— 一个泛型的约束依赖另一个
function mergeConfig<Base, Override extends Partial<Base>>(
  base: Base,
  override: Override
): Base & Override {
  return { ...base, ...override }
}

// 模式2:高阶泛型 —— 泛型参数本身是泛型
type WithId<T> = T & { id: string }
type WithTimestamps<T> = T & { createdAt: Date; updatedAt: Date }

type FullEntity<T> = WithTimestamps<WithId<T>>
// FullEntity<{ name: string }> =
//   { name: string; id: string; createdAt: Date; updatedAt: Date }

// 模式3:工厂函数返回泛型
function createStore<T>(initial: T) {
  let state = initial
  return {
    get: (): T => state,
    set: (next: T): void => { state = next },
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

第7部分:类型编程基础 —— keyof、typeof 与索引访问 ​

7.1 keyof —— 获取键的联合类型 ​

┌─────────────────────────────────────────────────────────────┐
│              keyof 运算符                                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  interface User {                                           │
│    id: string                                               │
│    name: string                                             │
│    age: number                                              │
│  }                                                          │
│                                                             │
│  type UserKey = keyof User                                  │
│  // "id" | "name" | "age"                                  │
│                                                             │
│  keyof 任何对象类型 → 其所有键的字面量联合                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts
// keyof 的典型应用:类型安全的属性访问
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user: User = { id: "1", name: "Ada", age: 30 }
const name = getProperty(user, "name")  // name: string
// getProperty(user, "email")           // ❌ 编译错误
1
2
3
4
5
6
7
8

7.2 typeof —— 从值获取类型 ​

TypeScript 的 typeof 有两层含义:JavaScript 运行时的 typeof 和 TypeScript 类型层面的 typeof。

ts
// JS 运行时 typeof:返回字符串
console.log(typeof "hello")  // "string"

// TS 类型层面 typeof:从值提取类型
const config = {
  host: "localhost",
  port: 8080,
  retry: 3,
}

type Config = typeof config
// { host: string; port: number; retry: number }

// 典型用法:从已有值推导相关类型
function makeStore<T>(initial: T) {
  return { get: () => initial, set: (v: T) => { /* ... */ } }
}

const userStore = makeStore({ id: "1", name: "Ada" })
type UserStore = typeof userStore
// { get: () => { id: string; name: string }; set: (v: { id: string; name: string }) => void }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

7.3 索引访问类型 ​

ts
// T[K] —— 获取 T 中键 K 对应的值类型
interface User {
  id: string
  name: string
  tags: string[]
  profile: {
    avatar: string
    bio: string
  }
}

type UserId = User["id"]           // string
type UserTags = User["tags"]       // string[]
type UserProfile = User["profile"] // { avatar: string; bio: string }

// 联合键获取联合值类型
type StringFields = User["id" | "name"]  // string

// 嵌套索引
type AvatarType = User["profile"]["avatar"]  // string

// 与 keyof 组合——获取所有值类型的联合
type UserValues = User[keyof User]
// string | string[] | { avatar: string; bio: string }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

7.4 typeof + keyof + 索引访问的组合 ​

ts
const routes = {
  home: "/",
  about: "/about",
  user: "/user/:id",
} as const

// 获取所有路由路径的类型
type RoutePath = (typeof routes)[keyof typeof routes]
// "/" | "/about" | "/user/:id"

// 这是从运行时常量推导类型的最强模式
1
2
3
4
5
6
7
8
9
10
11

第8部分:映射类型 —— 转换对象形状 ​

8.1 基本映射类型 ​

┌─────────────────────────────────────────────────────────────┐
│              映射类型的语法                                  │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  { [K in 联合类型]: 值类型 }                                │
│                                                             │
│  K 遍历联合类型中的每个成员,为每个 K 生成一个属性           │
│                                                             │
│  输入:{ id: string; name: string; age: number }            │
│                                                             │
│  type Nullable<T> = { [K in keyof T]: T[K] | null }        │
│                                                             │
│  输出:{ id: string | null; name: string | null;             │
│          age: number | null }                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
ts
// 基础映射:每个属性变成只读
type MyReadonly<T> = {
  readonly [K in keyof T]: T[K]
}

// 每个属性变成可选
type MyPartial<T> = {
  [K in keyof T]?: T[K]
}
1
2
3
4
5
6
7
8
9

8.2 修饰符:-readonly 和 -? ​

ts
// 去除只读
type Mutable<T> = {
  -readonly [K in keyof T]: T[K]
}

// 去除可选
type Required<T> = {
  [K in keyof T]-?: T[K]
}

// 组合:同时去除 readonly 和 ?
type MutableAndRequired<T> = {
  -readonly [K in keyof T]-?: T[K]
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

8.3 键重映射(Key Remapping via as) ​

TypeScript 4.1 引入了 as 子句,可以在映射过程中重命名键。

┌─────────────────────────────────────────────────────────────┐
│              键重映射的三种经典模式                          │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  模式1:添加前缀/后缀                                       │
│  type WithPrefix<T> = {                                    │
│    [K in keyof T as `on${Capitalize<string & K>}`]: T[K]   │
│  }                                                          │
│  // { name: string } → { onName: string }                  │
│                                                             │
│  模式2:过滤键                                              │
│  type OnlyStrings<T> = {                                   │
│    [K in keyof T as T[K] extends string ? K : never]       │
│      : T[K]                                                 │
│  }                                                          │
│  // 只保留值为 string 类型的属性                            │
│                                                             │
│  模式3:基于值类型变换键                                     │
│  type EventHandlers<T> = {                                 │
│    [K in keyof T as T[K] extends (...args: any[]) => any   │
│      ? K : never]: T[K]                                     │
│  }                                                          │
│  // 只保留函数类型的属性                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ts
// 实际例子:Getter 类型
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}

interface Person {
  name: string
  age: number
}

type PersonGetters = Getters<Person>
// {
//   getName: () => string
//   getAge: () => number
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

第9部分:条件类型与 infer ​

9.1 基本条件类型 ​

条件类型的语法类似三元表达式,但运行在类型层面:

┌─────────────────────────────────────────────────────────────┐
│              条件类型语法                                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  T extends U ? X : Y                                        │
│                                                             │
│  如果 T 是 U 的子类型 → 结果为 X                            │
│  否则 → 结果为 Y                                            │
│                                                             │
│  示例:                                                     │
│  type IsString<T> = T extends string ? true : false        │
│  IsString<"hello">  →  true                                │
│  IsString<42>       →  false                               │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

9.2 分配条件类型(Distributive Conditional Types) ​

当条件类型的检查对象是裸泛型参数且传入联合类型时,条件类型会自动分配到每个成员。

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

// 传入联合类型 → 分配
type Result = ToArray<string | number>
// 等价于:ToArray<string> | ToArray<number>
// 结果:string[] | number[]

// 阻止分配:用方括号包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never
type Result2 = ToArrayNoDistribute<string | number>
// 结果:(string | number)[]
1
2
3
4
5
6
7
8
9
10
11

9.3 infer —— 在条件类型中提取类型 ​

infer 是类型编程中最强大的工具之一:在 extends 子句中声明一个待推断的类型变量。

ts
// 提取数组元素类型
type ElementType<T> = T extends (infer U)[] ? U : never
type E1 = ElementType<string[]>       // string
type E2 = ElementType<number>         // never

// 提取函数返回类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type R1 = MyReturnType<() => string>   // string

// 提取 Promise 内部类型
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T
type A1 = MyAwaited<Promise<Promise<number>>>  // number

// 提取函数第一个参数类型
type FirstParam<T> = T extends (first: infer F, ...rest: any[]) => any
  ? F : never
type P1 = FirstParam<(a: string, b: number) => void>  // string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
┌─────────────────────────────────────────────────────────────┐
│              infer 的常见使用场景                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  T extends Array<infer U>        → 提取数组元素             │
│  T extends Promise<infer U>      → 提取 Promise 值          │
│  T extends (...args: any[])      → 提取函数返回类型         │
│    => infer R                                               │
│  T extends (...args: infer P)    → 提取函数参数元组         │
│    => any                                                  │
│  T extends { data: infer D }     → 提取对象属性             │
│  T extends `${infer A}.${infer B}` → 提取模板字面量部分     │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

第10部分:模板字面量类型 ​

10.1 基本语法 ​

TypeScript 4.1 引入了模板字面量类型,将字符串模式匹配带入了类型系统。

ts
type World = "world"
type Greeting = `hello ${World}`  // "hello world"

// 联合类型在模板中自动分配(笛卡尔积)
type Color = "red" | "blue"
type Size = "sm" | "lg"
type ButtonVariant = `${Color}-${Size}`
// "red-sm" | "red-lg" | "blue-sm" | "blue-lg"
1
2
3
4
5
6
7
8

10.2 模式匹配与提取 ​

ts
// 提取字符串的特定部分
type GetLastName<T extends string> =
  T extends `${infer First} ${infer Last}` ? Last : never

type L = GetLastName<"Ada Lovelace">  // "Lovelace"

// 提取文件扩展名
type FileExtension<T extends string> =
  T extends `${string}.${infer Ext}` ? Ext : never

type Ext = FileExtension<"app.config.ts">  // "ts"

// 递归提取路径段
type SplitPath<T extends string> =
  T extends `${infer Seg}/${infer Rest}`
    ? Seg | SplitPath<Rest>
    : T

type Segments = SplitPath<"a/b/c">  // "a" | "b" | "c"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

10.3 与映射类型组合 ​

ts
// 为对象的每个属性创建 setter 函数类型
type Setters<T> = {
  [K in keyof T & string as `set${Capitalize<K>}`]: (value: T[K]) => void
}

interface Config {
  theme: "light" | "dark"
  fontSize: number
}

type ConfigSetters = Setters<Config>
// {
//   setTheme: (value: "light" | "dark") => void
//   setFontSize: (value: number) => void
// }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

10.4 内置字符串操作类型 ​

ts
// TypeScript 内置了四个字符串操作类型
type A = Uppercase<"hello">   // "HELLO"
type B = Lowercase<"HELLO">   // "hello"
type C = Capitalize<"hello">  // "Hello"
type D = Uncapitalize<"Hello">  // "hello"
1
2
3
4
5

第11部分:类型收窄的全套工具箱 ​

11.1 收窄策略总览 ​

┌─────────────────────────────────────────────────────────────┐
│              TypeScript 类型收窄工具箱                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  策略                 适用场景             收窄依据           │
│  ──────────────────────────────────────────────────────     │
│  typeof val           原始类型             JS typeof 结果    │
│  instanceof Cls       类实例              原型链             │
│  "key" in obj         对象属性存在性       hasOwn + proto    │
│  val === literal      精确值比较          严格相等           │
│  val != null          null/undefined 排除  不等比较           │
│  if (val)             真值检查            强制布尔转换       │
│  Array.isArray(val)   数组检查            Array.isArray      │
│  discriminated union  状态建模            switch + 判别      │
│  type predicate       自定义类型守卫      返回值是 x is T    │
│  assertion function   断言函数            返回值是 asserts   │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

11.2 类型谓词(Type Predicates) ​

类型谓词 x is T 是自定义类型守卫的返回值类型。它告诉 TypeScript:如果这个函数返回 true,那么参数就是类型 T。

ts
interface Cat {
  meow(): void
}
interface Dog {
  bark(): void
}

// 类型谓词:返回类型是 value is Cat
function isCat(value: unknown): value is Cat {
  return (
    typeof value === "object" &&
    value !== null &&
    "meow" in value &&
    typeof (value as Cat).meow === "function"
  )
}

function handleAnimal(animal: Cat | Dog) {
  if (isCat(animal)) {
    animal.meow()  // animal: Cat
  } else {
    animal.bark()  // animal: Dog
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
┌─────────────────────────────────────────────────────────────┐
│              类型谓词的使用要点                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 谓词函数内部实现必须正确——TS 不验证实现的正确性         │
│     → 写得不对 = 运行时类型错误 + 编译器不报错              │
│     → 谓词是"对编译器的承诺",责任在开发者                  │
│                                                             │
│  2. 不要过度使用类型谓词来绕过类型检查                      │
│     → 能用 typeof / instanceof 解决的优先用                 │
│     → 复杂的数据验证应该在运行时用 schema 库                │
│                                                             │
│  3. 类型谓词可以组合                                        │
│     function isStringOrNumber(x: unknown)                   │
│       : x is string | number { ... }                        │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

11.3 断言函数(Assertion Functions) ​

TypeScript 3.7 引入了断言函数,使用 asserts x is T 语法。与类型谓词不同,断言函数不返回布尔值,而是通过抛出异常来表示"不满足条件"。

ts
// 断言函数:如果 value 不是 string,抛出异常
function assertString(value: unknown): asserts value is string {
  if (typeof value !== "string") {
    throw new TypeError(`Expected string, got ${typeof value}`)
  }
}

declare const input: unknown
assertString(input)
// 从这里开始,input 的类型是 string
input.toUpperCase()  // ✅

// 另一种形式:asserts condition
function assert(condition: unknown, message?: string): asserts condition {
  if (!condition) {
    throw new Error(message ?? "Assertion failed")
  }
}

declare const x: string | undefined
assert(x !== undefined)
// 从这里开始,x 的类型是 string
x.toUpperCase()  // ✅
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

第12部分:as const、satisfies 与常量类型参数 ​

12.1 as const —— 深度只读字面量推断 ​

as const 做三件事:将属性标记为 readonly,将字面量类型收窄到精确值,将数组变为只读元组。

ts
// 不加 as const:类型被拓宽
const routes1 = { home: "/", user: "/user" }
// { home: string; user: string }

// 加 as const:类型精确收窄
const routes2 = { home: "/", user: "/user" } as const
// { readonly home: "/"; readonly user: "/user" }

// 数组的区别
const arr1 = ["a", "b", "c"]
// string[]

const arr2 = ["a", "b", "c"] as const
// readonly ["a", "b", "c"]

// 典型场景:从常量对象推导精确联合类型
const colors = ["red", "green", "blue"] as const
type Color = (typeof colors)[number]  // "red" | "green" | "blue"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

12.2 satisfies 运算符(TS 4.9+) ​

satisfies 检查类型兼容性但不改变表达式的推断类型。这是"验证而不拓宽"的最优雅方式。

ts
// 场景:希望 colors 满足 string[],但保留字面量类型
const colors = ["red", "green", "blue"] satisfies string[]
// colors 的类型仍是 readonly ["red", "green", "blue"]
// 但 TypeScript 验证了它满足 string[] 约束

type ColorName = (typeof colors)[number]  // "red" | "green" | "blue"

// 对比:不加 satisfies
const colors2 = ["red", "green", "blue"]
// colors2: string[]
// 字面量类型丢失!

// 对比:加类型注解
const colors3: string[] = ["red", "green", "blue"]
// colors3: string[]
// 字面量类型丢失!

// satisfies 最适合:既要约束,又要字面量类型
const config = {
  host: "localhost",
  port: 8080,
  retry: 3,
  verbose: true,
} satisfies Record<string, string | number | boolean>
// config 保留了每个属性的精确字面量类型
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
┌─────────────────────────────────────────────────────────────┐
│              satisfies 使用场景                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ 既需要类型检查,又需要保留字面量类型                     │
│     → 如颜色列表、路由定义、配置常量                         │
│                                                             │
│  ✅ 验证一个对象满足某个接口,但不拓宽类型                   │
│     → 如组件的 props 默认值                                 │
│                                                             │
│  ❌ 如果不需要保留字面量类型,直接使用类型注解更清晰        │
│     → 如函数参数、模块导出                                  │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14

12.3 const 类型参数(TS 5.0+) ​

ts
// 在泛型参数前加 const 修饰符,等价于调用时自动加 as const
function useState<const T>(initial: T): [T, (v: T) => void] {
  /* ... */
}

const [count, setCount] = useState(0)
// count: 0(字面量类型),而不是 number

// 对比:不加 const 修饰符
function useState2<T>(initial: T): [T, (v: T) => void] {
  /* ... */
}
const [count2] = useState2(0)
// count2: number
1
2
3
4
5
6
7
8
9
10
11
12
13
14

第13部分:工具类型速查与实践 ​

13.1 属性操作类 ​

ts
interface User {
  id: string
  name: string
  email?: string
  readonly createdAt: Date
}

// Partial<T> —— 所有属性变为可选
type UserPatch = Partial<User>
// { id?: string; name?: string; email?: string;
//   readonly createdAt?: Date }

// Required<T> —— 所有属性变为必需
type FullUser = Required<User>
// { id: string; name: string; email: string;
//   readonly createdAt: Date }

// Readonly<T> —— 所有属性变为只读
type ImmutableUser = Readonly<User>
// { readonly id: string; readonly name: string;
//   readonly email?: string; readonly createdAt: Date }

// Pick<T, K> —— 选取指定属性
type UserBrief = Pick<User, "id" | "name">
// { id: string; name: string }

// Omit<T, K> —— 排除指定属性
type UserWithoutEmail = Omit<User, "email">
// { id: string; name: string; readonly createdAt: Date }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

13.2 联合操作类 ​

ts
// Record<K, V> —— 以 K 为键、V 为值的对象类型
type PageRoutes = Record<"home" | "about" | "contact", string>
// { home: string; about: string; contact: string }

// Exclude<T, U> —— 从 T 中排除可赋值给 U 的类型
type Primitive = Exclude<string | number | Date | null, object | null>
// string | number

// Extract<T, U> —— 从 T 中提取可赋值给 U 的类型
type Obj = Extract<string | number | Date | null, object>
// Date

// NonNullable<T> —— 从 T 中排除 null 和 undefined
type RequiredValue = NonNullable<string | null | undefined>
// string
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

13.3 函数类型操作类 ​

ts
// ReturnType<F> —— 提取函数返回类型
type FetchResult = ReturnType<typeof fetch>
// Promise<Response>

// Parameters<F> —— 提取函数参数元组
type FetchParams = Parameters<typeof fetch>
// [input: RequestInfo | URL, init?: RequestInit]

// Awaited<T> —— 解开 Promise 链
type FetchData = Awaited<ReturnType<typeof fetch>>
// Response(不是 Promise<Response>)

// 实战组合
declare function getUser(id: string): Promise<{ name: string; age: number }>
type UserResult = Awaited<ReturnType<typeof getUser>>
// { name: string; age: number }
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第14部分:声明文件与模块增强 ​

14.1 .d.ts 文件的作用 ​

┌─────────────────────────────────────────────────────────────┐
│              .d.ts 文件的三种用途                            │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 为 JS 库提供类型描述                                    │
│     → DefinitelyTyped (@types/*) 就是 .d.ts 的集合          │
│                                                             │
│  2. 声明全局类型和变量                                       │
│     → 如 process.env 的类型扩展                             │
│                                                             │
│  3. 模块增强(Module Augmentation)                         │
│     → 为第三方库添加类型                                    │
│                                                             │
│  关键规则:                                                 │
│  .d.ts 中只有类型声明,不能有可执行代码                     │
│  declare 关键字声明"存在于运行时但 TS 不知道的东西"         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

14.2 declare global 与全局扩展 ​

ts
// global.d.ts
declare global {
  // 扩展全局变量
  var __VERSION__: string

  // 扩展 Window
  interface Window {
    __INITIAL_STATE__: Record<string, unknown>
  }

  // 扩展 Array(谨慎使用)
  interface Array<T> {
    last(): T | undefined
  }
}

// 让文件作为模块(否则 declare global 在某些配置下失效)
export {}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

14.3 模块增强 ​

ts
// 为第三方库 express 添加自定义属性
declare module "express" {
  interface Request {
    currentUser?: {
      id: string
      role: "admin" | "user"
    }
  }
}

// 声明非 JS/TS 模块
declare module "*.svg" {
  const content: React.FunctionComponent<React.SVGAttributes<SVGElement>>
  export default content
}

declare module "*.css" {
  const classes: Record<string, string>
  export default classes
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

第15部分:品牌类型 —— 在结构类型中模拟名义类型 ​

15.1 为什么要品牌类型 ​

┌─────────────────────────────────────────────────────────────┐
│              品牌类型解决的问题                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  问题:两个 string 可能是完全不同的东西                     │
│                                                             │
│  function transferMoney(                                      │
│    fromAccountId: string,                                    │
│    toAccountId: string,                                      │
│    amount: number                                           │
│  ) { ... }                                                  │
│                                                             │
│  // 以下调用都能编译通过,但可能错误                        │
│  transferMoney(userId, accountId, amount)  // userId 不是 accountId
│  transferMoney(accountId, userName, amount) // userName 不是 accountId
│                                                             │
│  品牌类型解决:                                             │
│  transferMoney(                                     │
│    fromAccountId: AccountId,                                 │
│    toAccountId: AccountId,                                   │
│    amount: Money                                            │
│  ) { ... }                                                  │
│  // → 编译时就能发现参数错位                                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

15.2 实现品牌类型 ​

ts
// 品牌类型的核心:交叉一个独特的品牌属性
type Brand<T, B> = T & { __brand: B }

type UserId = Brand<string, "UserId">
type AccountId = Brand<string, "AccountId">
type Money = Brand<number, "Money">

function createUserId(id: string): UserId {
  return id as UserId  // 唯一的品牌创建点
}

function createAccountId(id: string): AccountId {
  return id as AccountId
}

function transferMoney(from: AccountId, to: AccountId, amount: Money): void {
  // 现在类型安全了
}

// 使用
const acc1 = createAccountId("acc-001")
const acc2 = createAccountId("acc-002")
const amt = 100 as Money

transferMoney(acc1, acc2, amt)  // ✅

// transferMoney(createUserId("u-1"), acc2, amt)  // ❌ UserId != AccountId
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
┌─────────────────────────────────────────────────────────────┐
│              品牌类型的适用场景                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ ID 类型(UserId, OrderId, ProductId)                   │
│  ✅ 单位类型(Meters, Seconds, Pixels)                     │
│  ✅ 验证后的数据(ValidatedEmail, SanitizedHtml)           │
│  ✅ 货币类型(USD, EUR, CNY)                               │
│                                                             │
│  ⚠️ 品牌在运行时不存在,序列化后丢失                        │
│     → 序列化/反序列化时需要重新品牌化                       │
│                                                             │
│  ⚠️ 不能阻止所有误用(如 as 强制转换)                      │
│     → 品牌类型是"护栏"而非"安全门"                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

第16部分:tsconfig.json 深度解析 ​

16.1 strict 及其子选项 ​

strict: true 是 TypeScript 推荐的默认起点。它开启以下全部子选项:

┌─────────────────────────────────────────────────────────────┐
│              strict 家族                                     │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  strict: true 等价于同时开启:                              │
│                                                             │
│  ┌─────────────────────────────────────────────────────┐   │
│  │  strictNullChecks        null/undefined 不能赋给     │   │
│  │                          非空类型                    │   │
│  │  strictFunctionTypes     函数参数双向协变检查       │   │
│  │  strictBindCallApply     bind/call/apply 参数检查   │   │
│  │  strictPropertyInitialization  类属性必须初始化      │   │
│  │  noImplicitAny           禁止隐式 any               │   │
│  │  noImplicitThis          禁止隐式 this 类型         │   │
│  │  alwaysStrict            输出 "use strict"          │   │
│  └─────────────────────────────────────────────────────┘   │
│                                                             │
│  建议:新项目直接 strict: true,不要逐个关闭子选项          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

16.2 重要但非默认的选项 ​

jsonc
{
  "compilerOptions": {
    // ===== 强烈推荐 =====

    // 索引访问时包含 undefined(数组越界的真相)
    "noUncheckedIndexedAccess": true,

    // 可选属性不允许显式赋 undefined
    // { x?: number } 中 x 只能是 number 或省略,不能是 undefined
    "exactOptionalPropertyTypes": true,

    // ===== 模块相关 =====
    // 现代项目的模块语法
    "module": "ESNext",
    "moduleResolution": "bundler",  // 或 "node16" / "nodenext"

    // 确保 import 类型和值的语法明确
    "verbatimModuleSyntax": true,

    // ===== 其他质量选项 =====
    // 未使用的局部变量报错
    "noUnusedLocals": true,
    // 未使用的参数报错
    "noUnusedParameters": true,
    // 有返回类型的函数必须所有分支都有返回值
    "noImplicitReturns": true,
    // switch 的 fall-through 报错
    "noFallthroughCasesInSwitch": true,
    // 捕获的变量必须使用
    "noUncheckedSideEffectImports": true,
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

16.3 moduleResolution 的选择 ​

┌─────────────────────────────────────────────────────────────┐
│              moduleResolution 选择指南                       │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  bundler(推荐,TS 5.0+)                                  │
│  → 模拟打包器(webpack/vite/esbuild)的解析行为            │
│  → 支持无扩展名导入、package.json exports                  │
│  → 适合绝大多数现代前端项目                                 │
│                                                             │
│  node16 / nodenext                                           │
│  → 严格遵循 Node.js ESM/CJS 解析规则                       │
│  → 必须使用 .js 扩展名导入                                 │
│  → 适合 Node.js 库和工具                                    │
│                                                             │
│  node(遗留)                                               │
│  → 旧版 Node.js 解析(CJS 为主)                           │
│  → 不支持 package.json exports                             │
│  → 不推荐新项目使用                                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

第17部分:运行时验证边界 —— 类型系统的边界 ​

17.1 TypeScript 类型在运行时消失 ​

这是 TypeScript 类型系统最根本的边界:编译后所有类型注解被擦除,运行时没有任何类型信息。

┌─────────────────────────────────────────────────────────────┐
│              编译前后对比                                    │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  编译前(.ts):                                            │
│  interface User {                                           │
│    id: string                                               │
│    name: string                                             │
│  }                                                          │
│  function greet(user: User): string {                       │
│    return `Hello, ${user.name}`                             │
│  }                                                          │
│                                                             │
│  编译后(.js):                                            │
│  function greet(user) {                                     │
│    return `Hello, ${user.name}`                             │
│  }                                                          │
│  // interface User 完全消失                                 │
│  // 参数类型注解完全消失                                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

17.2 必须运行时验证的数据源 ​

以下数据在编译时类型未知,必须在运行时验证:

  • HTTP 请求与响应(REST API、WebSocket)
  • 环境变量(process.env)
  • 数据库查询结果和历史数据
  • 消息队列事件(Kafka、RabbitMQ、SQS)
  • 本地存储(localStorage、IndexedDB)
  • 第三方 SDK 返回值
  • 用户输入、文件上传、URL 参数

17.3 Zod / Valibot 验证模式 ​

┌─────────────────────────────────────────────────────────────┐
│              Schema 验证 + 类型推导模式                      │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│                    ┌───────────┐                            │
│  外部数据 ────────→│ Zod Schema│────→ 验证通过的数据        │
│  (unknown)       └───────────┘     (类型安全)            │
│                         │                                    │
│                         │ z.infer<typeof schema>            │
│                         ↓                                    │
│                    编译时类型                                │
│                    (单一真源头)                            │
│                                                             │
│  核心思想:schema 是唯一的真相来源                          │
│  类型从 schema 推导,而非手写 interface 再单独验证          │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ts
import { z } from "zod"

// 1. 定义 schema(运行时 + 编译时的单一真相来源)
const UserSchema = z.object({
  id: z.string().uuid(),
  name: z.string().min(1),
  email: z.string().email().optional(),
  role: z.enum(["admin", "user", "viewer"]),
  createdAt: z.string().datetime(),
})

// 2. 从 schema 推导类型(编译时)
type User = z.infer<typeof UserSchema>
// { id: string; name: string; email?: string;
//   role: "admin" | "user" | "viewer"; createdAt: string }

// 3. 运行时验证
async function handleRequest(body: unknown): Promise<Response> {
  const result = UserSchema.safeParse(body)

  if (!result.success) {
    return Response.json(
      { error: "Validation failed", details: result.error.issues },
      { status: 400 }
    )
  }

  // result.data 的类型是 User,完全类型安全
  const user: User = result.data
  return Response.json({ user })
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
ts
// Valibot 的等价写法(tree-shakeable,适合前端)
import * as v from "valibot"

const UserSchema = v.object({
  id: v.pipe(v.string(), v.uuid()),
  name: v.pipe(v.string(), v.minLength(1)),
  email: v.optional(v.pipe(v.string(), v.email())),
  role: v.picklist(["admin", "user", "viewer"]),
  createdAt: v.pipe(v.string(), v.isoTimestamp()),
})

type User = v.InferOutput<typeof UserSchema>
1
2
3
4
5
6
7
8
9
10
11
12

核心总结 ​

类型系统设计哲学 ​

┌─────────────────────────────────────────────────────────────┐
│              TypeScript 类型系统的三大支柱                   │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  1. 结构类型(Structural Typing)                           │
│     → 形状决定兼容性,而非名称或继承                       │
│     → 最小约束原则:只声明真正需要的字段                   │
│                                                             │
│  2. 类型收窄(Type Narrowing)                              │
│     → typeof / instanceof / in / 判别联合 / 类型谓词       │
│     → 从宽类型逐步收窄到精确类型                           │
│                                                             │
│  3. 类型编程(Type-Level Programming)                      │
│     → keyof / typeof / 索引访问 / 映射 / 条件 / infer      │
│     → 在类型层面表达复杂约束和转换                         │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

类型系统边界 ​

┌─────────────────────────────────────────────────────────────┐
│              类型安全的完整图景                              │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  编译时(TypeScript)    │  运行时(JavaScript)            │
│                          │                                   │
│  ┌──────────────────┐   │  ┌──────────────────────┐        │
│  │  类型注解         │   │  │  Schema 验证(Zod)   │        │
│  │  类型推断         │   │  │  边界检查             │        │
│  │  泛型约束         │   │  │  防御性编程           │        │
│  │  类型收窄         │   │  │  try-catch            │        │
│  │  条件类型         │   │  │  默认值处理           │        │
│  └──────────────────┘   │  └──────────────────────┐        │
│         ↓                │         ↓                │        │
│  保证内部逻辑正确        │  保证外部数据安全        │        │
│                          │                                   │
│  两者互补,缺一不可                                       │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

最佳实践速查 ​

┌─────────────────────────────────────────────────────────────┐
│              TypeScript 最佳实践清单                         │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ✅ strict: true 是起点,不是目标                           │
│  ✅ 局部变量靠推断,函数边界写类型                          │
│  ✅ unknown > any(99%的场景)                              │
│  ✅ 用判别联合建模状态,不用布尔标志的组合                  │
│  ✅ 泛型表达关系,不是减少字符                             │
│  ✅ 对象形状用 interface,类型运算用 type                  │
│  ✅ 跨界数据必须运行时验证(Zod/Valibot)                  │
│  ✅ 不要共用一个万能类型——数据库行 ≠ API 响应 ≠ 表单       │
│  ✅ public API 类型保持简单——让调用方容易理解错误          │
│  ✅ as const / satisfies 保留字面量类型                    │
│                                                             │
└─────────────────────────────────────────────────────────────┘
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

章节测试 ​

选择题 ​

  1. TypeScript 的类型系统属于哪一类? A. 名义类型 B. 结构类型 C. 鸭子类型 D. 强类型

  2. 以下哪项不是 as const 的效果? A. 属性变为 readonly B. 字面量类型收窄到精确值 C. 数组变为只读元组 D. 对象变为不可扩展

  3. 关于泛型的"只用一次"启发式,以下哪种用法是正确的? A. function foo<T>(x: T): void —— T 只用于参数类型 B. function map<T, U>(arr: T[], fn: (v: T) => U): U[] C. function bar<T extends string>(x: T): T —— T 只在参数中 D. function logger<T>(data: T): void —— T 只用于参数

  4. 以下哪种情况不能通过 keyof 获取键的联合类型? A. interface B. type 别名 C. 联合类型 D. class

  5. [T] extends [any] ? T[] : never 中,方括号的作用是什么? A. 强制 T 为数组 B. 阻止条件类型的分配 C. 提取元组元素 D. 类型断言

简答题 ​

  1. 简述 unknown 和 any 的区别,并说明何时必须使用 unknown。

  2. 写出一个判别联合类型来建模 HTTP 请求状态(idle / pending / success / error),并为每个变体提供合适的字段。


参考答案 ​

  1. B. 结构类型 —— TypeScript 使用结构类型系统,类型兼容性由形状决定。

  2. D. 对象变为不可扩展 —— as const 不会阻止向对象添加新属性(在运行时),它只影响编译时的类型推断。

  3. B. —— T 和 U 都出现了至少两次,表达了数组元素类型到返回数组元素类型的映射关系。A/C/D 的泛型参数各只出现一次,没有建立关系。

  4. C. 联合类型 —— keyof 作用于对象类型,联合类型没有固定的键集合。keyof (A | B) 的结果是 keyof A 和 keyof B 的共有键。

  5. B. 阻止条件类型的分配 —— 当 T 是裸泛型参数且传入联合类型时,条件类型会分配。用 [T] 包裹后 T 不再是裸参数,分配行为被抑制。

  6. any 关闭所有类型检查,可以从它访问任意属性、赋给任意类型,不会有编译错误。unknown 是类型安全的顶层类型:不能直接使用,必须先通过类型收窄确定具体类型。使用场景:处理外部输入(API 响应、用户输入、JSON.parse)时,先用 unknown 接收,再通过 schema 验证或类型守卫收窄到具体类型。

  7. ts
    1

type HttpState<T> = | { status: "idle" } | { status: "pending"; abortController: AbortController } | { status: "success"; data: T; statusCode: number } |


---

## 相关笔记

- [[01-javascript-runtime-and-language]] —— JavaScript 运行时模型和语言核心语义,是 TypeScript 的运行时基础
- [[03-async-and-event-loop]] —— 异步编程中 `Promise<T>` 的类型建模与错误处理模式

---

## 下一步学习

- [ ] 在新项目中启用 `strict: true` + `noUncheckedIndexedAccess: true` + `exactOptionalPropertyTypes: true`
- [ ] 将项目中的 `any` 逐一替换为 `unknown` 或具体类型
- [ ] 为一个现有的 API 响应类型添加 Zod schema 验证层
- [ ] 用判别联合重构项目中一处用布尔标志组合建模的状态
- [ ] 阅读 [TypeScript Handbook - Type Manipulation](https://www.typescriptlang.org/docs/handbook/2/types-from-types.html)
- [ ] 阅读 [type-challenges](https://github.com/type-challenges/type-challenges) 并完成前 10 道 Easy 题目

**学习状态**:🟡 开始学习
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

最后更新于:

Pager
上一篇2. JavaScript 核心语义:值、对象、函数与模块 / JavaScript Core Semantics: Values, Objects, Functions, and Modules
下一篇4. JavaScript 异步编程与事件循环 / Asynchronous JavaScript and the Event Loop

持续记录,持续成长

Copyright © Tidenflow