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 Useras 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
否则
↓
返回 Y9. infer:从类型中提取信息
例如:
type ReturnType<T> =
T extends (...args: any[]) => infer R
? R
: neverinfer 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 添加类型注释。