我的文档库

TypeScript

从类型系统、类型推导到类型级编程,理解 TypeScript 如何参与大型前端工程设计

TypeScript 真正值得学习的地方,不是 interface、type、enum 这些语法,而是:

TypeScript 如何在 JavaScript 之上建立一套静态类型系统,以及这套类型系统如何参与大型工程的设计。

为什么 TypeScript 能在没有运行时信息的情况下推导出一个函数的返回类型?

为什么 any 会让类型系统逐渐失效?

为什么条件类型可以实现类型级别的模式匹配?

为什么一个看似正确的泛型 API,最后会导致整个项目的类型复杂度爆炸?


1. TypeScript 到底解决了什么问题?

JavaScript 最大的问题之一,是类型错误通常发生在运行时。

function add(a, b) {
  return a + b
}

add(1, 2)
add('1', 2)

JavaScript 并不会阻止第二次调用。

TypeScript 的核心思路是:

JavaScript
    ↓
类型分析
    ↓
静态检查
    ↓
JavaScript

注意:

TypeScript 的类型并不会默认存在于最终 JavaScript 运行时。

因此:

interface User {
  id: number
  name: string
}

编译后不会产生:

class User {}

因为 interface 本身不是运行时实体。

这也解释了一个非常重要的问题:

TypeScript 能不能保证运行时数据一定符合类型?

不能。

例如:

interface User {
  id: number
  name: string
}

const user = await fetch('/api/user').then(res => res.json())

const data = user as User

as User 并没有验证服务器返回的数据。

它只是告诉 TypeScript:

“相信我。”

因此:

TypeScript
    ↓
编译期正确性

而:

Schema Validator
    ↓
运行时正确性

大型项目通常需要二者结合。


2. 类型系统的核心:结构类型

TypeScript 采用的是 Structural Typing(结构类型系统)。

例如:

interface User {
  id: number
  name: string
}

const user = {
  id: 1,
  name: 'Hamy',
  age: 18
}

const data: User = user

这里可以成立。

因为 TypeScript 不关心:

“你是不是 User 创建出来的?”

它关心:

“你的结构是不是满足 User?”

因此:

User
├── id: number
└── name: string

只要一个对象至少满足这个结构,就可以赋值。


3. 为什么函数参数会出现“逆变”?

interface Animal {
  name: string
}

interface Dog extends Animal {
  bark(): void
}

假设:

type Handler<T> = (value: T) => void

那么:

const animalHandler: Handler<Animal> = animal => {
  console.log(animal.name)
}

一个只能处理 Dog 的函数:

const dogHandler: Handler<Dog> = dog => {
  dog.bark()
}

不能安全地替换一个可以处理任意 Animal 的函数。

因为调用方可能传:

animalHandler(cat)

但:

dogHandler(cat)

会出问题。

所以函数参数位置存在类型安全上的逆变问题。

因为函数类型描述的是“能够接受什么输入”,如果允许参数类型向更具体的方向替换,就可能把一个实际不支持的数据传给函数,从而破坏类型安全。


4. 泛型真正解决的是什么?

泛型不是:

function identity<T>(value: T) {
  return value
}

这么简单。

真正重要的是:

泛型允许类型之间建立关系。

例如:

function first<T>(items: T[]): T | undefined {
  return items[0]
}

这里最重要的不是 T。

而是:

输入 T[]
    ↓
输出 T

类型关系被保留下来了。


5. 泛型约束

大型工程中,泛型通常需要约束。

function getId<T extends { id: string }>(value: T) {
  return value.id
}

这里:

T extends { id: string }

不是说:

T 是这个类型。

而是:

T 至少满足这个结构。

因此:

getId({
  id: '1',
  name: 'Hamy'
})

是合法的。


6. keyof:从对象结构提取 Key

interface User {
  id: string
  name: string
  age: number
}

type UserKey = keyof User

得到:

'id' | 'name' | 'age'

于是可以写:

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

现在:

const user = {
  id: '1',
  name: 'Hamy',
  age: 18
}

const name = getValue(user, 'name')

TypeScript 可以推导:

name: string

而:

getValue(user, 'xxx')

会直接报错。

这就是类型系统真正的价值:

让 API 的使用方式本身成为类型约束。


7. Indexed Access Type

type UserName = User['name']

得到:

string

更有意思的是:

type UserValue = User[keyof User]

得到:

string | number

这意味着 TypeScript 可以对类型进行类似“索引访问”。


8. 条件类型:TypeScript 的模式匹配

条件类型:

T extends U ? X : Y

例如:

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

于是:

type A = IsString<string>
// true

type B = IsString<number>
// false

可以把它理解成:

如果 T 满足 U
    ↓
返回 X
否则
    ↓
返回 Y

9. infer:从类型中提取信息

例如:

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

infer R 的意思是:

如果 T 是函数,那么把它的返回值类型提取出来。

例如:

type Fn = () => Promise<User>

type Result = ReturnType<Fn>

得到:

Promise<User>

10. 分布式条件类型

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

当:

type Result = ToArray<string | number>

TypeScript 会分发:

string[] | number[]

而不是:

(string | number)[]

可以理解成:

ToArray<string>
    |
ToArray<number>
    ↓
string[] | number[]

这也是很多高级工具类型的基础。


11. satisfies 为什么重要?

假设:

type Config = {
  theme: 'light' | 'dark'
}

如果:

const config: Config = {
  theme: 'light'
}

变量会被约束为 Config。

而:

const config = {
  theme: 'light'
} satisfies Config

则表达的是:

“请检查这个值满足 Config,但尽可能保留它自身更精确的类型。”

这是大型配置对象中非常实用的模式。


12. 类型系统最容易被什么破坏?

最常见的是:

any

例如:

const data: any = await request()

从这里开始:

data
 ↓
any
 ↓
传播
 ↓
更多 any
 ↓
类型安全逐渐失效

所以大型工程应该尽量:

unknown

而不是:

any

例如:

function parse(value: unknown) {
  if (
    typeof value === 'object' &&
    value !== null
  ) {
    // ...
  }
}

unknown 强迫你先证明类型。


13. TypeScript 真正的工程边界

一个成熟项目应该把类型系统分成几层:

API Schema
    ↓
Domain Type
    ↓
ViewModel Type
    ↓
Component Props
    ↓
UI State

而不是:

API Response
      ↓
any
      ↓
React

推荐:

Server Schema
      ↓
Runtime Validation
      ↓
Domain Model
      ↓
TypeScript
      ↓
UI

这样 TypeScript 才真正成为工程基础设施。


14. 其他?

Q1:TypeScript 为什么不能保证运行时安全?

因为类型系统主要参与编译期检查,网络响应、用户输入、LocalStorage 等运行时数据仍然是不可信的。


Q2:unknown 和 any 的区别?

any 会关闭类型检查。

unknown 表示“类型未知”,使用前必须进行类型收窄。


Q3:never 有什么作用?

never 表示:

不可能存在的值。

典型场景:

function assertNever(value: never): never {
  throw new Error('Unexpected value')
}

用于保证联合类型处理完整。


Q4:为什么 TypeScript 类型越复杂不一定越好?

因为类型系统本身也存在复杂度。

如果一个 API:

type X<
  A,
  B extends keyof A,
  C extends ...
> = ...

已经很难被团队理解,那么类型系统可能已经开始反过来增加维护成本。

高级工程的目标不是:

“类型越强越好。”

而是:

在正确性、可读性、推导能力和编译成本之间取得平衡。


15. 最终应该建立什么能力?

学完 TypeScript,不应该停留在:

interface
type
enum
generic

而应该建立:

JavaScript
   ↓
类型系统
   ↓
类型关系
   ↓
API 设计
   ↓
Domain 建模
   ↓
工程边界

真正高级的 TypeScript,是:

用类型设计系统,而不是给 JavaScript 添加类型注释。

本页目录