TypeScript 高级类型
泛型基础与高级用法
泛型是 TypeScript 类型系统中最核心的抽象机制。它允许在定义函数、接口、类时不预先指定具体类型,而在使用时再传入类型参数,从而实现类型安全的代码复用。泛型的本质是类型层面的参数化多态(Parametric Polymorphism)——将类型作为参数传递,让同一段逻辑适用于不同的类型。
泛型函数
function identity<T>(arg: T): T {
return arg
}
const str = identity('hello')
const num = identity(42)当调用 identity('hello') 时,TypeScript 编译器通过类型参数推断(Type Argument Inference) 自动将 T 推断为 string,无需手动标注 identity<string>('hello')。推断的本质是编译器在调用点将实参类型与形参类型进行统一化(Unification) 运算,解出类型变量的绑定。
泛型接口与泛型类
interface Repository<T> {
getById(id: string): T
getAll(): T[]
save(entity: T): void
}
class NumberRepository implements Repository<number> {
private data: Map<string, number> = new Map()
getById(id: string): number {
return this.data.get(id) ?? 0
}
getAll(): number[] {
return Array.from(this.data.values())
}
save(entity: number): void {
this.data.set(String(entity), entity)
}
}泛型类的静态成员不能引用类型参数,因为静态成员属于类构造函数本身而非实例。类型参数仅在实例侧的类型空间中有效。
泛型约束
通过 extends 关键字对泛型参数施加约束,限制其必须满足某种结构。
interface Lengthwise {
length: number
}
function logLength<T extends Lengthwise>(arg: T): T {
console.log(arg.length)
return arg
}
logLength('hello')
logLength([1, 2, 3])
logLength({ length: 10, value: 'test' })约束的本质是对类型参数建立一个上界(Upper Bound)。T extends Lengthwise 意味着 T 必须是 Lengthwise 的子类型——即结构上至少包含 length: number 属性。
更强大的约束模式——使用类型参数约束另一个类型参数:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const person = { name: 'Alice', age: 30 }
const name = getProperty(person, 'name')
const age = getProperty(person, 'age')这里 K extends keyof T 确保 key 必须是 obj 的已知属性名之一,返回类型 T[K] 是索引访问类型,精确地表达了"取对象某个属性的值类型"这一语义。
泛型默认值
泛型参数可以指定默认类型,当调用方未显式传入且编译器无法推断时,使用默认值。
interface ApiResponse<T = unknown> {
code: number
message: string
data: T
}
const res1: ApiResponse = { code: 200, message: 'ok', data: 'anything' }
const res2: ApiResponse<string[]> = { code: 200, message: 'ok', data: ['a', 'b'] }泛型默认值遵循与函数默认参数相同的规则——有默认值的类型参数必须排在没有默认值的类型参数之后:
type CreateElement<Tag extends string, Props = {}, Children extends any[] = []> = {
tag: Tag
props: Props
children: Children
}
type Div = CreateElement<'div'>
type Button = CreateElement<'button', { disabled: boolean }>
type List = CreateElement<'ul', {}, [string, string]>泛型推断的高级场景
TypeScript 的类型推断在涉及多个泛型参数和复杂数据流时会进行多阶段推断(Multi-phase Inference)。理解推断的优先级和方向性,对编写类型友好的 API 至关重要。
function merge<T, U>(a: T, b: U): T & U {
return { ...a, ...b }
}
const result = merge({ name: 'Alice' }, { age: 30 })当推断出现冲突时,TypeScript 会尝试寻找最佳公共类型(Best Common Type):
function firstElement<T>(arr: T[]): T | undefined {
return arr[0]
}
const mixed = firstElement([1, 'hello', true])此时 T 被推断为 string | number | boolean,因为数组字面量的元素类型被联合化处理。
泛型推断在回调函数中的上下文类型推断(Contextual Typing):
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn)
}
const lengths = map(['hello', 'world'], item => item.length)item 的类型被从 arr 的实参推断出 T = string,然后流入回调函数的参数类型,使得 item 自动获得 string 类型。这是 TypeScript 推断引擎中协同推断(Co-inference) 的典型体现。
条件类型
条件类型是 TypeScript 2.8 引入的核心特性,它将类型层面的 if-else 逻辑引入了类型系统。其语法形式为 T extends U ? X : Y,语义是:如果 T 可赋值给 U,则类型为 X,否则为 Y。条件类型使得 TypeScript 的类型系统成为了图灵完备的——它本质上是一种类型层面的模式匹配与递归计算工具。
基本条件类型
type IsString<T> = T extends string ? true : false
type A = IsString<string>
type B = IsString<number>
type C = IsString<'hello'>A 为 true,B 为 false,C 为 true(字面量类型 'hello' 是 string 的子类型)。
分布式条件类型
当条件类型中的被检查类型(T)是一个裸类型参数(Naked Type Parameter) 且被实例化为联合类型时,条件类型会自动分布(Distribute) 到联合的每个成员上。
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>Result 的类型是 string[] | number[],而非 (string | number)[]。分布过程等价于:
type Result = (string extends any ? string[] : never) | (number extends any ? number[] : never)关键概念:"裸类型参数"是指 T 直接出现在 extends 左侧,未被任何其他类型包裹。一旦 T 被包裹,分布行为就会被阻止:
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never
type Result2 = ToArrayNonDist<string | number>Result2 的类型是 (string | number)[],因为 [T] 将 T 包裹在了元组中,阻止了分布。
利用分布式条件类型的经典应用——过滤联合类型:
type Filter<T, U> = T extends U ? never : T
type Result3 = Filter<'a' | 'b' | 'c' | 'd', 'a' | 'c'>Result3 为 'b' | 'd'。这正是内置工具类型 Exclude 的实现原理。
infer 关键字
infer 用于在条件类型中声明一个待推断的类型变量。它只能出现在 extends 子句中,作用是让编译器从实际类型中提取(Extract) 出某个位置的类型。
type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never
type R1 = ReturnTypeOf<() => string>
type R2 = ReturnTypeOf<(x: number) => boolean>
type R3 = ReturnTypeOf<string>R1 为 string,R2 为 boolean,R3 为 never。
infer 提取函数参数类型:
type FirstParam<T> = T extends (first: infer P, ...rest: any[]) => any ? P : never
type P1 = FirstParam<(name: string, age: number) => void>
type P2 = FirstParam<() => void>P1 为 string,P2 为 never。
infer 提取数组元素类型:
type ElementOf<T> = T extends (infer E)[] ? E : never
type E1 = ElementOf<string[]>
type E2 = ElementOf<[number, string, boolean]>E1 为 string,E2 为 number | string | boolean(元组展开为联合)。
infer 提取 Promise 内部类型:
type UnpackPromise<T> = T extends Promise<infer U> ? UnpackPromise<U> : T
type UP1 = UnpackPromise<Promise<string>>
type UP2 = UnpackPromise<Promise<Promise<number>>>
type UP3 = UnpackPromise<string>UP1 为 string,UP2 为 number(递归展开),UP3 为 string。
infer 在字符串模板字面量中的使用:
type TrimLeft<S extends string> = S extends ` ${infer Rest}` ? TrimLeft<Rest> : S
type Trimmed = TrimLeft<' hello'>Trimmed 为 'hello'。
infer 的协变与逆变位置对推断结果的影响:
type Foo<T> = T extends { a: infer U; b: infer U } ? U : never
type T1 = Foo<{ a: string; b: string }>
type T2 = Foo<{ a: string; b: number }>T1 为 string,T2 为 string | number。当同一个类型变量 U 出现在多个协变位置时,推断结果为联合类型。
type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void } ? U : never
type T3 = Bar<{ a: (x: string) => void; b: (x: string) => void }>
type T4 = Bar<{ a: (x: string) => void; b: (x: number) => void }>T3 为 string,T4 为 string & number(即 never)。当同一个类型变量 U 出现在多个逆变位置(函数参数位置)时,推断结果为交叉类型。
映射类型
映射类型(Mapped Types)允许基于已有类型创建新类型——遍历一个类型的所有属性键,并对每个属性进行转换。其语法形式为 { [P in K]: T },其中 P 是遍历变量,K 是键的联合类型,T 是每个属性的新类型。映射类型的本质是类型层面的 for...in 循环。
keyof 操作符
keyof 用于获取一个类型的所有公共属性键,返回一个字符串字面量(或数字字面量、symbol)的联合类型。
interface User {
name: string
age: number
email: string
}
type UserKeys = keyof UserUserKeys 的类型为 'name' | 'age' | 'email'。
keyof 作用于索引签名时的行为:
type StringIndex = keyof { [key: string]: unknown }
type NumberIndex = keyof { [key: number]: unknown }StringIndex 为 string | number(因为 JavaScript 中数字索引最终也会转为字符串),NumberIndex 为 number。
基本映射类型
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}
type Optional<T> = {
[P in keyof T]?: T[P]
}
type ReadonlyUser = Readonly<User>
type OptionalUser = Optional<User>in 关键字在映射类型中充当迭代器角色,逐一遍历联合类型中的每个成员。T[P] 是索引访问类型(Indexed Access Type),表示类型 T 中属性 P 对应的值类型。
修饰符操作(+/- 修饰符)
映射类型中可以使用 + 和 - 前缀来添加或移除 readonly 和 ? 修饰符。
type Mutable<T> = {
-readonly [P in keyof T]: T[P]
}
type Required<T> = {
[P in keyof T]-?: T[P]
}
interface Config {
readonly host: string
readonly port: number
database?: string
}
type MutableConfig = Mutable<Config>
type RequiredConfig = Required<Config>-readonly 移除只读修饰符,-? 移除可选修饰符。+readonly 和 +? 分别添加只读和可选修饰符(+ 可以省略,因为默认就是添加)。
as 子句重映射
TypeScript 4.1 引入了映射类型中的 as 子句,允许在遍历过程中对键进行转换。
type Getters<T> = {
[P in keyof T as `get${Capitalize<string & P>}`]: () => T[P]
}
interface Person {
name: string
age: number
}
type PersonGetters = Getters<Person>PersonGetters 的类型为 { getName: () => string; getAge: () => number }。
利用 as 子句过滤属性——当键被映射为 never 时,该属性会被移除:
type OmitByType<T, U> = {
[P in keyof T as T[P] extends U ? never : P]: T[P]
}
interface Mixed {
name: string
age: number
active: boolean
email: string
}
type WithoutStrings = OmitByType<Mixed, string>WithoutStrings 的类型为 { age: number; active: boolean }。
重映射结合模板字面量类型进行键的批量转换:
type EventMap<T> = {
[P in keyof T as `on${Capitalize<string & P>}Change`]: (value: T[P]) => void
}
type FormEvents = EventMap<{ name: string; age: number }>FormEvents 的类型为 { onNameChange: (value: string) => void; onAgeChange: (value: number) => void }。
模板字面量类型
模板字面量类型(Template Literal Types)是 TypeScript 4.1 引入的特性,它将 JavaScript 的模板字符串语法提升到了类型层面。通过模板字面量类型,可以在类型系统中进行字符串的拼接、拆分和模式匹配。
基本用法
type Greeting = `hello ${string}`
type World = `hello ${'world'}`
type EventName = `${'click' | 'scroll' | 'mousemove'}Handler`Greeting 匹配所有以 'hello ' 开头的字符串,World 精确为 'hello world',EventName 为 'clickHandler' | 'scrollHandler' | 'mousemoveHandler'。
当模板字面量类型中包含联合类型时,会产生笛卡尔积式的分布:
type Color = 'red' | 'blue'
type Size = 'small' | 'large'
type Style = `${Color}-${Size}`Style 为 'red-small' | 'red-large' | 'blue-small' | 'blue-large',是两个联合类型的完全组合。
内置字符串操作类型
TypeScript 提供了四个内置的字符串操作类型,它们是编译器内部实现的固有类型(Intrinsic Types),无法通过用户代码实现。
type U = Uppercase<'hello'>
type L = Lowercase<'HELLO'>
type C = Capitalize<'hello'>
type UC = Uncapitalize<'Hello'>U 为 'HELLO',L 为 'hello',C 为 'Hello',UC 为 'hello'。
它们在映射类型中的实际应用——将对象属性名转换为各种命名风格:
type CamelToSnake<S extends string> = S extends `${infer Head}${infer Tail}`
? Tail extends Uncapitalize<Tail>
? `${Lowercase<Head>}${CamelToSnake<Tail>}`
: `${Lowercase<Head>}_${CamelToSnake<Tail>}`
: S
type R = CamelToSnake<'getUserName'>R 为 'get_user_name'。这个实现通过递归逐字符扫描:如果下一个字符 Tail 的首字母是小写(等于 Uncapitalize<Tail>),说明没有到达驼峰边界,直接转小写拼接;否则说明遇到了大写字母边界,插入下划线。
模板字面量与 infer 的组合
模板字面量类型与 infer 结合可以实现字符串的解析和拆分:
type Split<S extends string, D extends string> = S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S]
type Parts = Split<'a-b-c-d', '-'>Parts 为 ['a', 'b', 'c', 'd']。
从路径字符串中提取参数:
type ExtractRouteParams<T extends string> = T extends `${string}:${infer Param}/${infer Rest}`
? { [K in Param | keyof ExtractRouteParams<Rest>]: string }
: T extends `${string}:${infer Param}`
? { [K in Param]: string }
: {}
type Params = ExtractRouteParams<'/user/:id/post/:postId'>Params 为 { id: string; postId: string }。
内置工具类型源码解析
TypeScript 内置了一系列工具类型(Utility Types),它们全部由条件类型、映射类型等基础特性组合而成。理解它们的源码实现,是掌握高级类型的关键。
Partial<T>
将所有属性变为可选:
type Partial<T> = {
[P in keyof T]?: T[P]
}通过映射类型遍历 T 的所有键,给每个属性添加 ? 修饰符。keyof T 获取所有公共属性键的联合,T[P] 获取每个键对应的值类型,? 将属性标记为可选(类型变为 T[P] | undefined)。
interface User {
name: string
age: number
}
type PartialUser = Partial<User>PartialUser 等价于 { name?: string; age?: number }。
Required<T>
将所有属性变为必选,是 Partial 的逆操作:
type Required<T> = {
[P in keyof T]-?: T[P]
}-? 语法移除可选修饰符。如果属性原本就是必选的,-? 不会产生副作用。
Readonly<T>
将所有属性变为只读:
type Readonly<T> = {
readonly [P in keyof T]: T[P]
}添加 readonly 修饰符后,属性在类型层面不可被重新赋值。需要注意的是 readonly 是浅层的——嵌套对象的属性仍然可变。
Pick<T, K>
从类型 T 中挑选指定的属性子集:
type Pick<T, K extends keyof T> = {
[P in K]: T[P]
}K extends keyof T 约束了 K 必须是 T 的属性键的子集。映射时只遍历 K 中的键,而非 keyof T 的所有键。
type UserBasic = Pick<User, 'name'>Omit<T, K>
从类型 T 中排除指定的属性:
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>Omit 是 Pick 和 Exclude 的组合。首先 Exclude<keyof T, K> 从 T 的所有键中排除 K,然后 Pick 从 T 中挑选剩余的键。注意 K 的约束是 keyof any(即 string | number | symbol)而非 keyof T,这意味着可以传入 T 上不存在的键而不会报错。
Record<K, T>
构造一个属性键为 K、值类型为 T 的对象类型:
type Record<K extends keyof any, T> = {
[P in K]: T
}type PageInfo = Record<'home' | 'about' | 'contact', { title: string; url: string }>Record 本质上是一个限制了键集合的映射类型,常用于构造字典或查找表。
Exclude<T, U>
从联合类型 T 中排除可赋值给 U 的成员:
type Exclude<T, U> = T extends U ? never : T利用分布式条件类型,对 T 联合类型的每个成员进行检查,如果该成员可赋值给 U,返回 never(从联合中移除),否则保留。
type T = Exclude<'a' | 'b' | 'c', 'a' | 'b'>T 为 'c'。
Extract<T, U>
从联合类型 T 中提取可赋值给 U 的成员,与 Exclude 互补:
type Extract<T, U> = T extends U ? T : nevertype T = Extract<string | number | boolean, string | number>T 为 string | number。
NonNullable<T>
从类型中排除 null 和 undefined:
type NonNullable<T> = T & {}这个实现利用了交叉类型的特性:T & {} 会将 null 和 undefined 从联合中过滤掉,因为 null & {} 和 undefined & {} 都是 never。早期版本的实现是 T extends null | undefined ? never : T,新版本使用交叉类型更为简洁。
type T = NonNullable<string | number | null | undefined>T 为 string | number。
ReturnType<T>
获取函数类型的返回值类型:
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any约束 T 必须是函数类型,然后通过 infer R 在返回值位置推断出返回类型。
type R = ReturnType<() => { name: string; age: number }>R 为 { name: string; age: number }。
Parameters<T>
获取函数类型的参数类型,以元组形式返回:
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : neverinfer P 在参数列表位置推断出整个参数的元组类型。
type P = Parameters<(name: string, age: number) => void>P 为 [name: string, age: number]。
InstanceType<T>
获取构造函数类型的实例类型:
type InstanceType<T extends abstract new (...args: any) => any> = T extends abstract new (...args: any) => infer R ? R : anyabstract new (...args: any) => any 匹配所有构造函数(包括抽象类),infer R 推断出构造函数创建的实例类型。
class Animal {
name: string = ''
}
type A = InstanceType<typeof Animal>A 为 Animal。注意需要 typeof Animal 获取 Animal 的构造函数类型,而非实例类型。
Awaited<T>
递归展开 Promise 类型,获取最终解析的值类型(TypeScript 4.5 引入):
type Awaited<T> = T extends null | undefined
? T
: T extends object & { then(onfulfilled: infer F, ...args: infer _): any }
? F extends (value: infer V, ...args: infer _) => any
? Awaited<V>
: never
: T这个实现的精妙之处在于:它不直接检查 Promise<T>,而是检查 then 方法的存在——即匹配所有 thenable 对象。首先排除 null | undefined,然后检查是否有 then 方法,如果有,则提取 onfulfilled 回调的参数类型,并递归展开。
type A = Awaited<Promise<string>>
type B = Awaited<Promise<Promise<number>>>
type C = Awaited<boolean | Promise<string>>A 为 string,B 为 number,C 为 boolean | string。
类型体操实战
类型体操(Type Gymnastics)是指利用 TypeScript 类型系统的组合能力,在纯类型层面实现复杂的逻辑变换。以下是几个经典的高级类型实现。
DeepPartial
将对象类型的所有属性(包括嵌套属性)递归地变为可选:
type DeepPartial<T> = T extends object
? T extends Function
? T
: { [P in keyof T]?: DeepPartial<T[P]> }
: T
interface NestedConfig {
database: {
host: string
port: number
credentials: {
username: string
password: string
}
}
cache: {
ttl: number
enabled: boolean
}
}
type PartialConfig = DeepPartial<NestedConfig>实现要点:首先检查 T 是否是 object 类型(排除原始类型),然后检查是否是 Function(函数也是 object,但不应被展开),最后对剩余的对象类型递归应用 DeepPartial。PartialConfig 中 database.credentials.username 等深层属性全部变为可选。
DeepReadonly
将对象类型的所有属性(包括嵌套属性)递归地变为只读:
type DeepReadonly<T> = T extends Function
? T
: T extends object
? { readonly [P in keyof T]: DeepReadonly<T[P]> }
: T
interface State {
user: {
name: string
preferences: {
theme: string
lang: string
}
}
items: number[]
}
type ImmutableState = DeepReadonly<State>ImmutableState 中所有层级的属性都被标记为 readonly。由于数组也是 object,number[] 会被转换为 readonly number[]({ readonly [P in keyof number[]]: ... } 会保留数组的结构并添加 readonly)。
TupleToUnion
将元组类型转换为联合类型:
type TupleToUnion<T extends readonly any[]> = T[number]
type Union = TupleToUnion<[string, number, boolean]>Union 为 string | number | boolean。实现原理是利用数字索引访问类型——T[number] 会获取元组中所有数字索引位置的值类型并联合。
另一种基于递归的实现方式,帮助理解其机制:
type TupleToUnionRecursive<T extends readonly any[]> = T extends [infer First, ...infer Rest]
? First | TupleToUnionRecursive<Rest>
: neverUnionToIntersection
将联合类型转换为交叉类型——这是类型体操中最经典的技巧之一:
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (x: infer I) => void
? I
: never
type Intersection = UnionToIntersection<{ a: string } | { b: number } | { c: boolean }>Intersection 为 { a: string } & { b: number } & { c: boolean }。
深度分析这个实现的两步机制:
第一步:U extends any ? (x: U) => void : never。利用分布式条件类型,将联合类型 { a: string } | { b: number } 分布展开为 ((x: { a: string }) => void) | ((x: { b: number }) => void)——即函数联合类型。
第二步:... extends (x: infer I) => void ? I : never。这一步将函数联合类型进行推断。infer I 出现在函数参数位置(逆变位置),当对函数联合类型的参数进行推断时,根据逆变性质,多个候选类型会被推断为交叉类型。这就是联合转交叉的核心机制。
IsEqual
精确判断两个类型是否完全相等:
type IsEqual<A, B> = (<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2)
? true
: false
type Test1 = IsEqual<string, string>
type Test2 = IsEqual<string, number>
type Test3 = IsEqual<{ a: string }, { a: string }>
type Test4 = IsEqual<any, unknown>
type Test5 = IsEqual<never, never>
type Test6 = IsEqual<true, boolean>Test1 为 true,Test2 为 false,Test3 为 true,Test4 为 false,Test5 为 true,Test6 为 false。
这个实现利用了 TypeScript 编译器内部的一个特性:当比较两个泛型条件类型时,编译器会检查其条件类型是否结构等价。<T>() => T extends A ? 1 : 2 和 <T>() => T extends B ? 1 : 2 仅在 A 和 B 完全相同时才被视为兼容。这比简单的 A extends B ? B extends A ? true : false : false 更精确,因为后者无法区分 any 和其他类型。
其他实战类型
将联合类型转换为元组(利用函数重载的特性):
type UnionToTuple<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? [...UnionToTuple<Exclude<T, R>>, R]
: []
type Tuple = UnionToTuple<'a' | 'b' | 'c'>Tuple 为 ['a', 'b', 'c'](顺序可能因编译器内部实现而异)。
获取联合类型的最后一个成员:
type LastOfUnion<T> = UnionToIntersection<
T extends any ? () => T : never
> extends () => infer R
? R
: never
type Last = LastOfUnion<'a' | 'b' | 'c'>Last 为 'c'。原理是将联合转为函数交叉类型(等效于函数重载),然后 infer 推断时取最后一个重载的返回类型。
协变与逆变
协变(Covariance)与逆变(Contravariance)描述的是类型间的子类型关系在复合类型构造过程中如何传递。这是类型系统理论的核心概念,直接影响 TypeScript 中函数赋值、泛型兼容性等行为。
基本概念
假设 Animal 是父类型,Dog 是子类型(Dog extends Animal),则:
- 协变(Covariant):
F<Dog>是F<Animal>的子类型——子类型关系保持方向 - 逆变(Contravariant):
F<Animal>是F<Dog>的子类型——子类型关系反转方向 - 不变(Invariant):二者之间没有子类型关系
- 双变(Bivariant):两个方向都成立
class Animal {
name: string = ''
}
class Dog extends Animal {
breed: string = ''
}
class Greyhound extends Dog {
speed: number = 0
}返回值协变
函数返回值处于协变位置——如果 Dog extends Animal,那么 () => Dog 可赋值给 () => Animal:
type CovariantProducer<T> = () => T
let animalProducer: CovariantProducer<Animal>
let dogProducer: CovariantProducer<Dog> = () => new Dog()
animalProducer = dogProducer这是安全的:期望返回 Animal 的地方,返回一个更具体的 Dog 不会破坏任何约定——Dog 拥有 Animal 的所有属性,调用方可以安全地使用。
函数参数逆变
函数参数处于逆变位置——如果 Dog extends Animal,那么 (x: Animal) => void 可赋值给 (x: Dog) => void:
type ContravariantConsumer<T> = (item: T) => void
let animalConsumer: ContravariantConsumer<Animal> = (animal: Animal) => {
console.log(animal.name)
}
let dogConsumer: ContravariantConsumer<Dog>
dogConsumer = animalConsumer这也是安全的:期望处理 Dog 的函数位置,放置一个能处理所有 Animal 的函数——它一定能处理 Dog,因为 Dog 是 Animal 的子类型。
strictFunctionTypes
TypeScript 在 2.6 版本引入了 strictFunctionTypes 编译选项(包含在 strict 中)。开启后,函数参数类型检查从双变变为逆变。
type Handler<T> = (event: T) => void
let animalHandler: Handler<Animal> = (a: Animal) => console.log(a.name)
let dogHandler: Handler<Dog> = (d: Dog) => console.log(d.breed)
animalHandler = dogHandler开启 strictFunctionTypes 时,最后一行会报错:Type 'Handler<Dog>' is not assignable to type 'Handler<Animal>'。因为 dogHandler 需要访问 breed 属性,但传入的可能只是一个 Animal,没有 breed。
需要注意的是,strictFunctionTypes 只对以函数类型语法((x: T) => void)声明的函数生效。以方法语法(method(x: T): void)声明的函数仍然保持双变,这是为了兼容 Array<T> 等内置类型的方法:
interface Comparer<T> {
compare(a: T, b: T): number
}
let animalComparer: Comparer<Animal> = {
compare: (a, b) => a.name.localeCompare(b.name)
}
let dogComparer: Comparer<Dog> = animalComparer方法语法下这段代码不会报错,即使在 strictFunctionTypes 开启时。
协变与逆变在泛型中的应用
理解协变逆变对泛型参数推断至关重要——特别是在 infer 关键字的使用中:
type CovariantInfer<T> = T extends { value: infer U } ? U : never
type ContravariantInfer<T> = T extends { handler: (x: infer U) => void } ? U : never
type CV = CovariantInfer<{ value: string } | { value: number }>
type CTV = ContravariantInfer<{ handler: (x: string) => void } | { handler: (x: number) => void }>CV 为 string | number(协变位置推断出联合),CTV 为 never(逆变位置推断出 string & number,即 never)。
类型守卫
类型守卫(Type Guards)是 TypeScript 中收窄(Narrowing) 类型的机制。当代码在运行时进行类型检查后,TypeScript 的控制流分析(Control Flow Analysis)能够在后续分支中自动收窄变量的类型。
typeof 守卫
typeof 操作符可以守卫原始类型:
function padLeft(value: string, padding: string | number): string {
if (typeof padding === 'number') {
return ' '.repeat(padding) + value
}
return padding + value
}在 if 分支中,padding 的类型从 string | number 收窄为 number;在 else 分支中自动收窄为 string。
typeof 能识别的类型有限:"string" | "number" | "bigint" | "boolean" | "symbol" | "undefined" | "object" | "function"。
instanceof 守卫
instanceof 用于检查原型链,适用于类实例的收窄:
class ApiError {
statusCode: number
constructor(statusCode: number) {
this.statusCode = statusCode
}
}
class NetworkError {
endpoint: string
constructor(endpoint: string) {
this.endpoint = endpoint
}
}
function handleError(error: ApiError | NetworkError) {
if (error instanceof ApiError) {
console.log(error.statusCode)
} else {
console.log(error.endpoint)
}
}in 守卫
in 操作符检查属性是否存在于对象中,适用于区分具有不同属性的对象类型:
interface Fish {
swim: () => void
}
interface Bird {
fly: () => void
}
function move(animal: Fish | Bird) {
if ('swim' in animal) {
animal.swim()
} else {
animal.fly()
}
}in 守卫在联合类型中特别有用——TypeScript 会根据属性的存在性将联合类型收窄到包含该属性的成员。
is 类型谓词(自定义守卫)
当内置的守卫机制不够用时,可以通过类型谓词(Type Predicate) 定义自定义的守卫函数。返回类型声明为 paramName is Type:
interface Cat {
meow: () => void
purr: () => void
}
interface Dog {
bark: () => void
fetch: () => void
}
function isCat(animal: Cat | Dog): animal is Cat {
return 'meow' in animal
}
function interact(animal: Cat | Dog) {
if (isCat(animal)) {
animal.purr()
} else {
animal.fetch()
}
}animal is Cat 告诉 TypeScript:当此函数返回 true 时,将参数的类型收窄为 Cat。
自定义守卫在数组过滤中的应用:
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'square'; side: number }
| { kind: 'triangle'; base: number; height: number }
function isCircle(shape: Shape): shape is Extract<Shape, { kind: 'circle' }> {
return shape.kind === 'circle'
}
const shapes: Shape[] = [
{ kind: 'circle', radius: 5 },
{ kind: 'square', side: 10 },
{ kind: 'circle', radius: 3 }
]
const circles = shapes.filter(isCircle)circles 的类型被正确推断为 { kind: 'circle'; radius: number }[]。如果不使用类型谓词,filter 返回的类型仍然是 Shape[],因为普通的布尔返回值无法传递类型信息。
可辨识联合(Discriminated Unions)
可辨识联合是一种结合了字面量类型和联合类型的模式,配合 switch 语句可以实现穷举的类型收窄:
type Action =
| { type: 'INCREMENT'; payload: number }
| { type: 'DECREMENT'; payload: number }
| { type: 'RESET' }
function reducer(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
case 'RESET':
return 0
}
}在每个 case 分支中,action 的类型被收窄到对应的联合成员。编译器能够识别 action.type 是判别属性(Discriminant Property)——一个在每个联合成员中都具有不同字面量类型的属性。
穷举性检查——利用 never 确保所有分支都被处理:
function assertNever(x: never): never {
throw new Error('Unexpected value: ' + x)
}
function reducer2(state: number, action: Action): number {
switch (action.type) {
case 'INCREMENT':
return state + action.payload
case 'DECREMENT':
return state - action.payload
case 'RESET':
return 0
default:
return assertNever(action)
}
}如果将来向 Action 联合中添加了新成员但忘记处理,default 分支中 action 的类型将不再是 never,assertNever 调用会产生编译错误,提醒开发者补充处理逻辑。
声明文件
声明文件(Declaration Files,.d.ts)是 TypeScript 类型系统与 JavaScript 生态之间的桥梁。它只包含类型信息而不包含运行时代码,告诉编译器某个 JavaScript 模块、全局变量或外部库的类型形状。
.d.ts 文件的本质
.d.ts 文件在编译时参与类型检查,但不会被编译为任何 JavaScript 输出。它是纯粹的类型元数据。TypeScript 编译器通过以下优先级查找声明文件:
- 项目内的
.d.ts文件 node_modules/@types下的声明包tsconfig.json中typeRoots和types指定的路径- 三斜线指令引用的文件
declare 关键字
declare 用于声明一个存在于运行时但类型系统尚不知道的值。它只是告诉编译器"相信我,这个东西在运行时是存在的"。
declare const API_BASE_URL: string
declare function greet(name: string): void
declare class EventEmitter {
on(event: string, listener: (...args: any[]) => void): this
emit(event: string, ...args: any[]): boolean
}
declare enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT'
}declare 声明不会产生任何 JavaScript 代码,它们在编译后完全消失。它的唯一作用是在类型空间中注册标识符和类型信息。
模块声明
当使用没有类型定义的第三方库时,可以通过 declare module 为其补充类型:
declare module 'some-untyped-lib' {
export function parse(input: string): Record<string, unknown>
export function stringify(data: Record<string, unknown>): string
export default class Client {
constructor(config: { baseUrl: string; timeout?: number })
request<T>(endpoint: string): Promise<T>
}
}模块声明也支持通配符匹配,常用于非 JS 资源的导入:
declare module '*.css' {
const classes: Record<string, string>
export default classes
}
declare module '*.svg' {
const content: string
export default content
}
declare module '*.png' {
const src: string
export default src
}模块增强(Module Augmentation)——扩展已有模块的类型:
import { AxiosRequestConfig } from 'axios'
declare module 'axios' {
interface AxiosRequestConfig {
retryCount?: number
retryDelay?: number
}
}通过在模块声明中重新声明同名接口,利用 TypeScript 的声明合并(Declaration Merging) 特性将新属性合并到原有接口中。
全局声明
declare global 用于在模块文件中向全局作用域注入类型:
export {}
declare global {
interface Window {
__APP_CONFIG__: {
apiUrl: string
env: 'development' | 'staging' | 'production'
}
}
interface Array<T> {
toSorted(compareFn?: (a: T, b: T) => number): T[]
}
function __DEV_TOOLS_HOOK__(action: string): void
}文件必须是模块(包含 import 或 export),declare global 才能生效。export {} 是最小的模块标记方式。全局声明修改了全局作用域中的类型,所有文件都能看到这些变更。
三斜线指令
三斜线指令是一种特殊的注释形式,用于声明文件之间的依赖关系。它必须出现在文件的最顶部(只能在其之前有其他三斜线指令或注释)。
/// <reference path="./global.d.ts" />
/// <reference types="node" />
/// <reference lib="es2021" />/// <reference path="..." />引用另一个声明文件,告诉编译器将该文件纳入编译。在模块系统出现之前,这是组织多文件项目的主要方式。/// <reference types="..." />引用一个@types包的声明,等价于在tsconfig.json的types中添加该包。/// <reference lib="..." />引用内置的 lib 声明文件,例如es2021会引入lib.es2021.d.ts。
在现代 TypeScript 项目中,三斜线指令的使用已经大幅减少,大多数场景可以通过 tsconfig.json 的 types、typeRoots 和 lib 选项替代。但在编写 .d.ts 声明包时,三斜线指令仍然是声明对其他类型包依赖的标准方式。
声明文件的编写策略
为一个全局变量型库编写声明:
declare namespace MyLib {
interface Config {
debug: boolean
version: string
}
function init(config: Config): void
function destroy(): void
const VERSION: string
}为一个既支持模块导入又挂载全局变量的 UMD 库编写声明:
export as namespace MyUMDLib
export interface Options {
timeout: number
retries: number
}
export function create(options: Options): Instance
export interface Instance {
execute<T>(task: () => T): Promise<T>
cancel(): void
}export as namespace MyUMDLib 表示当以脚本模式(非模块)使用时,库挂载为全局变量 MyUMDLib;当以模块方式导入时,正常使用 export 的内容。
声明文件中类型的组织原则:优先使用 interface 而非 type(interface 支持声明合并,便于使用方扩展);避免使用 export default(命名导出更利于 IDE 自动补全和重构);使用 namespace 组织相关的类型,避免顶层命名空间污染。