
TypeScript 的类型系统不仅可以给变量、函数参数加类型,还可以在类型层面对已有类型进行提取、转换和组合。
本文会按能力类别整理几个常见关键字和工具类型:is、infer、keyof、typeof、Pick、Readonly、Partial、Exclude、Awaited,以及几个常见类型体操示例。
功能
is 通常用于函数返回值类型中,用来定义类型谓词。
它的作用是告诉 TypeScript:如果这个函数返回 true,那么传入的参数就可以被收窄为某个具体类型。
使用场景
普通的布尔函数虽然能在运行时判断类型,但 TypeScript 不一定能根据它自动推断参数类型:
function isString(value: unknown): boolean {
return typeof value === 'string'
}
function toUpperCase(value: unknown) {
if (isString(value)) {
value.toUpperCase()
// Error: value 仍然是 unknown
}
}
使用 value is string 后,TypeScript 就知道 if 分支里的 value 是 string:
function isString(value: unknown): value is string {
return typeof value === 'string'
}
function toUpperCase(value: unknown) {
if (isString(value)) {
value.toUpperCase()
}
}
小结
is 的核心作用是:自定义类型守卫,让 TypeScript 在条件判断后正确缩小类型范围。
功能
infer 用于条件类型中,让 TypeScript 自动推断某个位置的类型,并把推断结果保存到一个临时类型变量中。
注意:infer 只能写在 extends 条件类型里。
场景一:获取函数返回值类型
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never
type T1 = MyReturnType<() => string>
// string
type T2 = MyReturnType<(name: string) => number>
// number
这里的 infer R 表示:如果 T 是一个函数类型,就把它的返回值类型推断为 R。
场景二:获取数组元素类型
type Flatten<T> = T extends Array<infer U> ? U : T
type T1 = Flatten<string[]>
// string
type T2 = Flatten<number[]>
// number
如果传入的是数组类型,就取出数组元素类型;否则返回原类型。
type T3 = Flatten<boolean>
// boolean
功能
keyof 可以把一个对象类型的所有属性名提取出来,组成联合类型。
示例
interface Person {
name: string
age: number
}
type PersonKeys = keyof Person
// 'name' | 'age'
const key1: PersonKeys = 'name'
const key2: PersonKeys = 'age'
const key3: PersonKeys = 'height'
// Error: Type '"height"' is not assignable to type 'keyof Person'
常见用途
keyof 经常和泛型一起使用,用来约束参数必须是对象中存在的 key:
function getValue<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key]
}
const person = {
name: 'Tom',
age: 18
}
const name = getValue(person, 'name')
// string
const age = getValue(person, 'age')
// number
功能
typeof 在 TypeScript 类型语境中,可以根据已有变量反推出它的类型。
示例一:获取对象类型
const person = {
name: 'Tom',
age: 18
}
type Person = typeof person
等价于:
type Person = {
name: string
age: number
}
示例二:获取函数类型
function toArray(value: number): number[] {
return [value]
}
type ToArray = typeof toArray
// (value: number) => number[]
功能
keyof 和 typeof 经常组合使用,用来从一个实际对象中提取所有 key。
示例
const COLORS = {
red: 'red',
blue: 'blue'
} as const
type ColorKey = keyof typeof COLORS
// 'red' | 'blue'
let color: ColorKey
color = 'red'
color = 'blue'
color = 'yellow'
// Error
小结
typeof COLORS:获取变量 COLORS 的类型keyof typeof COLORS:获取这个类型的所有 key这个组合很适合用在常量映射、枚举替代方案、配置对象等场景。
功能
Pick<T, K> 可以从类型 T 中挑选部分属性 K,组成一个新的类型。
使用场景
假设有一个完整的 Todo 类型:
interface Todo {
title: string
description: string
completed: boolean
}
如果只想要 title 和 completed:
type TodoPreview = Pick<Todo, 'title' | 'completed'>
等价于:
type TodoPreview = {
title: string
completed: boolean
}
实现方式
type MyPick<T, K extends keyof T> = {
[P in K]: T[P]
}
说明
这里的 K extends keyof T 表示:K 必须是 T 中存在的属性名,不能随便传一个不存在的 key。
功能
Readonly<T> 可以把类型 T 中的所有属性变成只读属性。
示例
interface Todo {
title: string
completed: boolean
}
const todo: Readonly<Todo> = {
title: 'Learn TypeScript',
completed: false
}
todo.title = 'Learn JavaScript'
// Error: Cannot assign to 'title' because it is a read-only property.
实现方式
type MyReadonly<T> = {
readonly [P in keyof T]: T[P]
}
注意点
Readonly<T> 默认是浅层只读。
type State = Readonly<{
user: {
name: string
}
}>
const state: State = {
user: {
name: 'Tom'
}
}
state.user = { name: 'Jerry' }
// Error
state.user.name = 'Jerry'
// 可以修改,因为 user 内部对象不是深层 readonly
如果需要深层只读,需要自己实现 DeepReadonly。
功能
Partial<T> 可以把类型 T 中的所有属性变成可选属性。
示例
interface Person {
name: string
age: number
}
function updatePerson(person: Partial<Person>) {
// ...
}
updatePerson({
name: 'Tom'
})
updatePerson({
age: 18
})
Partial<Person> 等价于:
type PartialPerson = {
name?: string
age?: number
}
实现方式
type MyPartial<T> = {
[P in keyof T]?: T[P]
}
常见场景
Partial 很适合用在更新数据的场景中,因为更新接口通常不要求传入完整对象。
功能
Exclude<T, U> 可以从联合类型 T 中排除可以赋值给 U 的类型。
示例
type MyTypes = 'name' | 'age' | 'height'
type Result = Exclude<MyTypes, 'name'>
// 'age' | 'height'
也可以一次排除多个:
type Result = Exclude<'a' | 'b' | 'c', 'a' | 'c'>
// 'b'
实现方式
type MyExclude<T, U> = T extends U ? never : T
说明
Exclude 的核心依赖条件类型的分发特性。
当 T 是联合类型时,TypeScript 会把联合类型中的每一项单独拿出来判断:
'a' extends 'a' | 'c' ? never : 'a'
'b' extends 'a' | 'c' ? never : 'b'
'c' extends 'a' | 'c' ? never : 'c'
最终得到:
never | 'b' | never
也就是:
'b'
功能
Awaited<T> 用来获取 Promise resolve 之后的类型。
它会模拟 await 的行为,并且可以递归展开嵌套的 Promise。
示例
type A = Awaited<Promise<string>>
// string
type B = Awaited<Promise<Promise<number>>>
// number
type C = Awaited<boolean | Promise<number>>
// boolean | number
简化实现
type MyAwaited<T> = T extends Promise<infer R> ? MyAwaited<R> : T
使用场景
当我们想获取异步函数的返回值类型时,Awaited 非常有用:
async function getUser() {
return {
name: 'Tom',
age: 18
}
}
type User = Awaited<ReturnType<typeof getUser>>
这里:
ReturnType<typeof getUser>
得到的是:
Promise<{
name: string
age: number
}>
再通过 Awaited 解开 Promise,最终得到真实数据类型。
功能
Omit<T, K> 和 Pick 相反,它会从类型 T 中排除某些属性。
示例
interface Todo {
title: string
description: string
completed: boolean
}
type TodoPreview = Omit<Todo, 'description'>
等价于:
type TodoPreview = {
title: string
completed: boolean
}
实现方式
type MyOmit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>
功能
把一个元组类型转换为对象类型,对象的 key 和 value 都来自元组中的每一项。
示例
const tuple = ['tesla', 'model 3'] as const
type Result = TupleToObject<typeof tuple>
期望结果:
type Result = {
tesla: 'tesla'
'model 3': 'model 3'
}
实现方式
type TupleToObject<T extends readonly PropertyKey[]> = {
[K in T[number]]: K
}
说明
这里的关键点是:
T[number]
它可以取出元组或数组中所有元素的联合类型。
type Tuple = ['a', 'b', 'c']
type Union = Tuple[number]
// 'a' | 'b' | 'c'
功能
实现一个通用类型 First<T>,获取数组或元组的第一个元素类型。
示例
type Arr1 = ['a', 'b', 'c']
type Arr2 = [3, 2, 1]
type Head1 = First<Arr1>
// 'a'
type Head2 = First<Arr2>
// 3
实现方式一:通过 length 判断空元组
type First<T extends readonly unknown[]> = T['length'] extends 0 ? never : T[0]
实现方式二:使用 infer 匹配元组结构
type First<T extends readonly unknown[]> = T extends readonly [infer F, ...unknown[]]
? F
: never
第二种写法更能体现元组模式匹配的思想。
功能
实现一个通用类型 Length<T>,获取元组的长度。
示例
type Tesla = ['tesla', 'model 3']
type SpaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON']
type TeslaLength = Length<Tesla>
// 2
type SpaceXLength = Length<SpaceX>
// 3
实现方式
type Length<T extends readonly unknown[]> = T['length']
注意点
如果传入的是普通数组,得到的长度类型会是 number:
type A = Length<string[]>
// number
如果传入的是元组,才能得到具体数字字面量类型:
type B = Length<[string, number]>
// 2
这些关键字和工具类型可以分成几类理解:
isinfer、keyof、typeofkeyof typeofPick、Readonly、Partial、OmitExclude、AwaitedTupleToObject、First、Length掌握它们之后,很多复杂类型都可以拆成几个基础能力的组合:
keyof + typeof
infer + extends
keyof + mapped type
Exclude + Pick
Awaited + ReturnType
TypeScript 类型系统看起来复杂,但本质上就是在类型层面对数据结构进行提取、判断、转换和组合。先理解这些基础工具,再去看高级类型体操,会轻松很多。
这篇对新手挺友好的,is、infer、keyof 这几个点终于不是只会背了,配着例子看一下就顺很多。
我觉得整体挺清楚的,不过 Awaited 那段如果再多讲一点和 ReturnType 的配合,应该会更完整一些。