工具类型
TypeScript 内置了一系列工具类型(Utility Types),用于对已有类型进行变换、提取和构造。它们基于泛型、映射类型和条件类型实现,是类型体操的基石。本章逐一拆解全部内置工具类型:定义、示例、使用场景与源码实现。
总览速查表
| 工具类型 | 功能 | 分类 |
|---|---|---|
Partial<T> |
所有属性变为可选 | 对象操作 |
Required<T> |
所有属性变为必选 | 对象操作 |
Readonly<T> |
所有属性变为只读 | 对象操作 |
Pick<T, K> |
选取指定属性 | 对象操作 |
Omit<T, K> |
排除指定属性 | 对象操作 |
Record<K, V> |
构造键值对类型 | 对象操作 |
Exclude<T, U> |
从联合中排除 | 联合操作 |
Extract<T, U> |
从联合中提取 | 联合操作 |
NonNullable<T> |
排除 null / undefined | 联合操作 |
ReturnType<T> |
获取函数返回类型 | 函数操作 |
Parameters<T> |
获取函数参数类型 | 函数操作 |
ConstructorParameters<T> |
获取构造函数参数类型 | 函数操作 |
InstanceType<T> |
获取实例类型 | 函数操作 |
ThisParameterType<T> |
获取 this 参数类型 | 函数操作 |
OmitThisParameter<T> |
移除 this 参数类型 | 函数操作 |
Awaited<T> |
解包 Promise 类型 | 异步操作 |
Uppercase<S> |
转大写 | 字符串操作 |
Lowercase<S> |
转小写 | 字符串操作 |
Capitalize<S> |
首字母大写 | 字符串操作 |
Uncapitalize<S> |
首字母小写 | 字符串操作 |
对象操作类型
Partial<T>
定义:将类型 T 的所有属性变为可选。
语法:Partial<T>
示例:
interface User {
name: string
age: number
email?: string
}
type PartialUser = Partial<User>
// { name?: string; age?: number; email?: string }
const partial: PartialUser = { name: "Willow" } // ✅ OK
使用场景:更新函数只需传入部分数据时,配合 Object.assign 或展开运算合并默认配置。
function updateUser(id: number, patch: Partial<User>): void {
// 只需要传入要修改的字段
}
updateUser(1, { name: "Ming" })源码实现:
type Partial<T> = { [P in keyof T]?: T[P] }💡 最佳实践:
Partial<T>不递归——嵌套对象的内部属性不会自动变为可选。如需深层可选,见文末"自定义工具类型模式"中的DeepPartial。
Required<T>
定义:将类型 T 的所有属性变为必选(移除 ? 修饰符)。
语法:Required<T>
示例:
interface Config {
host?: string
port?: number
}
type RequiredConfig = Required<Config>
// { host: string; port: number }
const config: RequiredConfig = { host: "localhost", port: 8080 } // ✅ OK
// const bad: RequiredConfig = { host: "localhost" } // ❌ port 缺失
使用场景:确保从 API 或配置中读取的数据已填充所有字段后才进入核心逻辑。
function startServer(config: Required<Config>): void {
// 此时 host 和 port 保证存在
console.log(`Listening at ${config.host}:${config.port}`)
}源码实现:
type Required<T> = { [P in keyof T]-?: T[P] }🔬 深入原理:
-?是映射类型中的修饰符移除语法——?表示可选,-?会将其移除,等价于"不再可选"。同理,-readonly可以移除只读。
Readonly<T>
定义:将类型 T 的所有属性变为只读。
语法:Readonly<T>
示例:
interface Point {
x: number
y: number
}
type FrozenPoint = Readonly<Point>
// { readonly x: number; readonly y: number }
const p: FrozenPoint = { x: 0, y: 0 }
// p.x = 1 // ❌ Cannot assign to 'x' because it is a read-only property
使用场景:冻结配置对象、防止意外修改,配合 as const 使用。
const DEFAULTS: Readonly<Config> = {
host: "0.0.0.0",
port: 3000,
}
// DEFAULTS.port = 8080 // ❌ 编译报错
源码实现:
type Readonly<T> = { readonly [P in keyof T]: T[P] }🔬 深入原理:和
Partial一样,Readonly<T>是浅层的——对于嵌套对象,只有顶层属性被标记为readonly,内部仍然可变。需要深层只读时自行实现DeepReadonly。
Pick<T, K>
定义:从类型 T 中选取由 K 指定的属性,构造新类型。
语法:Pick<T, K>,其中 K extends keyof T。
示例:
interface User {
id: number
name: string
age: number
email: string
}
type UserPreview = Pick<User, "id" | "name">
// { id: number; name: string }
const preview: UserPreview = { id: 1, name: "Ming" } // ✅ OK
使用场景:从完整实体类型中提取对外暴露的子集,如 API 响应裁剪、表单数据提取。
// 数据库实体 → API 公开字段
type PublicUser = Pick<User, "id" | "name">
// 提交表单只需部分字段
type SignupForm = Pick<User, "name" | "email">源码实现:
type Pick<T, K extends keyof T> = { [P in K]: T[P] }💡 最佳实践:当
K是单个字段时,Pick<T, "name">仍然是对象类型{ name: string },不会自动解包。如果只要值的类型,用索引访问T["name"]。
Omit<T, K>
定义:从类型 T 中排除由 K 指定的属性,构造新类型。
语法:Omit<T, K>,其中 K extends keyof any。
示例:
interface User {
id: number
name: string
password: string
email: string
}
type PublicUser = Omit<User, "password">
// { id: number; name: string; email: string }
const publicUser: PublicUser = { id: 1, name: "Ming", email: "ming@example.com" } // ✅ OK
使用场景:从类型中移除敏感信息(密码)、排除不需要的字段、与 Pick 互补。
// 排除敏感字段
type SafeUser = Omit<User, "password" | "email">
// 比 Pick 更方便:当要保留的字段远多于排除的字段时
type WithoutId = Omit<User, "id">源码实现(TypeScript 3.5 原始实现,基于 Pick + Exclude):
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>TypeScript 4.1+ 使用了 key remapping 重写,行为一致:
type Omit<T, K extends keyof any> = { [P in keyof T as Exclude<P, K>]: T[P] }🚨 陷阱:
Omit<T, K>的约束是K extends keyof any(即string | number | symbol),而不是K extends keyof T。这意味着你可以传入不属于T的键而不会报错——它只是没有排除任何东西。
Record<K, V>
定义:构造一个键类型为 K、值类型为 V 的对象类型。
语法:Record<K, V>,其中 K extends keyof any。
示例:
type Role = "admin" | "editor" | "viewer"
type Permissions = Record<Role, boolean>
// { admin: boolean; editor: boolean; viewer: boolean }
const perms: Permissions = {
admin: true,
editor: false,
viewer: false,
} // ✅ OK
使用场景:字典映射、枚举值到配置的映射表、键值对结构。
// 字典
type UserMap = Record<string, User>
// 配置表
type FeatureFlags = Record<string, boolean>
// 枚举映射
type StatusLabel = Record<"pending" | "done" | "failed", string>源码实现:
type Record<K extends keyof any, T> = { [P in K]: T }🚨 陷阱:
Record<string, string>的属性值只能是string,不能写数字。如果需要一个混合值类型的字典,用索引签名{ [key: string]: string | number }。const bad: Record<string, string> = { count: 42 } // ❌ const good: Record<string, string> = { name: "Ming" } // ✅
联合操作类型
Exclude<T, U>
定义:从联合类型 T 中排除所有可以赋值给 U 的成员。
语法:Exclude<T, U>
示例:
type All = "a" | "b" | "c" | "d"
type WithoutAB = Exclude<All, "a" | "b">
// "c" | "d"
type Numbers = 1 | 2 | 3 | 4
type Even = Exclude<Numbers, 1 | 3>
// 2 | 4
使用场景:从联合类型中删除特定成员,配合 Omit 的内部实现(Exclude<keyof T, K>)。
源码实现:
type Exclude<T, U> = T extends U ? never : T🔬 深入原理:
Exclude利用分布式条件类型(Distributive Conditional Types)。当T是裸联合类型时,条件类型会分发到每个成员上分别求值。匹配到U的成员返回never,而never在联合中会被自动移除——最终剩下的就是未被排除的成员。
Extract<T, U>
定义:从联合类型 T 中提取所有可以赋值给 U 的成员。
语法:Extract<T, U>
示例:
type All = string | number | boolean | null
type Primitives = Extract<All, string | number | boolean>
// string | number | boolean
type Events = "click" | "scroll" | "keyup" | "keydown"
type KeyEvents = Extract<Events, `key${string}`>
// "keyup" | "keydown"
使用场景:从联合中筛选出感兴趣的成员,如提取特定前缀的类型、从混合事件类型中过滤目标类型。
// 从混合响应类型中提取成功分支
type Response = { ok: true; data: string } | { ok: false; error: Error }
type Success = Extract<Response, { ok: true }>
// { ok: true; data: string }
源码实现:
type Extract<T, U> = T extends U ? T : neverNonNullable<T>
定义:从类型 T 中排除 null 和 undefined。
语法:NonNullable<T>
示例:
type MaybeString = string | null | undefined
type Clean = NonNullable<MaybeString>
// string
type UserId = NonNullable<number | undefined>
// number
使用场景:过滤可能为空的 API 返回类型,与 Array.filter 配合收紧类型。
// 过滤掉数组中的空值
const items: (string | null)[] = ["a", null, "b", undefined as unknown as null]
const nonNull: string[] = items.filter((x): x is string => x !== null)
// 或者用类型工具声明过滤后的数组元素类型
type CleanArray = NonNullable<(string | null)[]>[number]
// string
源码实现:
type NonNullable<T> = T extends null | undefined ? never : T💡 最佳实践:
NonNullable<T>只能去除null和undefined,不能去除void、never或其他 falsy 值。如果需要更广泛的过滤,自行编写条件类型。
函数操作类型
ReturnType<T>
定义:提取函数类型 T 的返回值类型。
语法:ReturnType<T>,其中 T extends (...args: any[]) => any。
示例:
function getUser() {
return { id: 1, name: "Ming" }
}
type User = ReturnType<typeof getUser>
// { id: number; name: string }
const user: User = getUser() // ✅ OK
使用场景:当函数类型由外部定义(第三方库、自动生成代码),需要在调用处复用其返回类型时。
// 工厂函数返回类型复用
function createStore() {
return {
count: 0,
increment: () => {},
decrement: () => {},
}
}
type Store = ReturnType<typeof createStore>源码实现:
type ReturnType<T extends (...args: any[]) => any> =
T extends (...args: any[]) => infer R ? R : never🔬 深入原理:
infer R在条件类型的extends子句中声明一个类型变量,由 TypeScript 自动推断。ReturnType用它从函数签名末尾"抓取"返回值类型。
Parameters<T>
定义:提取函数类型 T 的参数类型,返回一个元组。
语法:Parameters<T>,其中 T extends (...args: any[]) => any。
示例:
function greet(name: string, age: number): string {
return `${name} is ${age}`
}
type GreetArgs = Parameters<typeof greet>
// [name: string, age: number]
const args: GreetArgs = ["Ming", 25]使用场景:复用函数的参数类型,如编写包装器(wrapper)或代理函数时保持参数签名一致。
// 包装函数,保持一模一样的参数列表
function logWrapper(fn: (...args: any[]) => any, ...args: Parameters<typeof fn>) {
console.log("Called with", args)
return fn(...args)
}源码实现:
type Parameters<T extends (...args: any[]) => any> =
T extends (...args: infer P) => any ? P : neverConstructorParameters<T>
定义:提取构造函数类型 T 的参数类型,返回一个元组。
语法:ConstructorParameters<T>,其中 T extends abstract new (...args: any[]) => any。
示例:
class Person {
constructor(public name: string, public age: number) {}
}
type PersonArgs = ConstructorParameters<typeof Person>
// [name: string, age: number]
const args: PersonArgs = ["Ming", 30]
const person = new Person(...args)使用场景:编写工厂函数或 IoC 容器时,需要动态构造实例并保持参数类型安全。
function factory<T extends abstract new (...args: any[]) => any>(
Ctor: T,
...args: ConstructorParameters<T>
): InstanceType<T> {
return new Ctor(...args)
}源码实现:
type ConstructorParameters<T extends abstract new (...args: any[]) => any> =
T extends abstract new (...args: infer P) => any ? P : neverInstanceType<T>
定义:提取构造函数类型 T 的实例类型。
语法:InstanceType<T>,其中 T extends abstract new (...args: any[]) => any。
示例:
class Person {
name = "Ming"
age = 30
}
type PersonInstance = InstanceType<typeof Person>
// Person
const p: PersonInstance = new Person() // ✅ OK
使用场景:从构造函数引用反向获得实例类型,配合 ConstructorParameters 编写泛型工厂。
// 从构造函数引用获取实例类型,无需 import 具体类型
function di<T extends abstract new (...args: any[]) => any>(Ctor: T): InstanceType<T> {
return new Ctor()
}源码实现:
type InstanceType<T extends abstract new (...args: any[]) => any> =
T extends abstract new (...args: any[]) => infer R ? R : neverThisParameterType<T>
定义:提取函数类型 T 中显式声明的 this 参数类型。
语法:ThisParameterType<T>
示例:
function handleClick(this: HTMLElement, e: MouseEvent) {
this.classList.toggle("active")
}
type Context = ThisParameterType<typeof handleClick>
// HTMLElement
使用场景:从已有方法中提取其上下文类型,用于类型安全的 call / apply / bind。
type HandlerCtx = ThisParameterType<typeof handleClick>
const button: HandlerCtx = document.createElement("button")
handleClick.call(button, new MouseEvent("click"))源码实现:
type ThisParameterType<T> =
T extends (this: infer U, ...args: any[]) => any ? U : unknownOmitThisParameter<T>
定义:从函数类型 T 中移除显式的 this 参数,返回去掉 this 后的函数签名。
语法:OmitThisParameter<T>
示例:
function handleClick(this: HTMLElement, e: MouseEvent) {
this.classList.toggle("active")
}
type Unbound = OmitThisParameter<typeof handleClick>
// (e: MouseEvent) => void
使用场景:将方法提取为独立函数类型,用于赋值给普通变量或在解绑上下文的场景中复用。
// 绑定 this 后赋值给独立函数
const bound = handleClick.bind(document.body)
// bound 的类型是 (e: MouseEvent) => void
源码实现:
type OmitThisParameter<T> =
T extends (this: any, ...args: infer A) => infer R ? (...args: A) => R : T异步操作类型
Awaited<T>
定义:递归解包 Promise 类型,获取其最终决议值类型。
语法:Awaited<T>
示例:
type P = Promise<string>
type S = Awaited<P>
// string
// 递归解包嵌套 Promise
type Nested = Promise<Promise<number>>
type Num = Awaited<Nested>
// number
使用场景:在 async 函数中获取返回值类型,或从第三方库的 Promise 返回中提取内层类型。
async function fetchUser(): Promise<{ id: number; name: string }> {
return await fetch("/api/user").then(r => r.json())
}
type User = Awaited<ReturnType<typeof fetchUser>>
// { id: number; name: string }
源码实现:
type Awaited<T> =
T extends null | undefined ? T : // 特殊处理 null/undefined
T extends object & { then(onfulfilled: infer F, ...args: any[]): any } ?
F extends (value: infer V, ...args: any[]) => any ? Awaited<V> : never :
T🔬 深入原理:
Awaited不仅递归解包 Promise,还处理了类thenable对象。它检查T是否具有符合PromiseLike的then方法,如果是就通过infer提取回调参数类型,再递归解包。
字符串操作类型
TypeScript 4.1 引入了四个字符串模板字面量类型操作。它们都是编译器内置实现(intrinsic),无法用用户代码实现。
Uppercase<S>
将字符串字面量类型转为全大写。
type Shout = Uppercase<"hello"> // "HELLO"
type Upper = Uppercase<"TypeScript"> // "TYPESCRIPT"
Lowercase<S>
将字符串字面量类型转为全小写。
type Whisper = Lowercase<"HELLO"> // "hello"
type Lower = Lowercase<"TypeScript"> // "typescript"
Capitalize<S>
将字符串字面量类型的首字母转为大写。
type Title = Capitalize<"hello world"> // "Hello world"
type ClassName = Capitalize<"button"> // "Button"
Uncapitalize<S>
将字符串字面量类型的首字母转为小写。
type Id = Uncapitalize<"Button"> // "button"
type VarName = Uncapitalize<"MyComponent"> // "myComponent"
使用场景:上述类型通常配合**模板字面量类型(Template Literal Types)**使用,实现字符串级别的类型变换。
// 自动生成 getter 名称
type EventName = "click" | "scroll"
type HandlerName = `on${Capitalize<EventName>}`
// "onClick" | "onScroll"
// 驼峰转属性名
type PropName<T extends string> = `$${Uncapitalize<T>}`
type LinkProp = PropName<"Component">
// "$component"
💡 最佳实践:四个字符串工具类型均无法用
infer或映射类型在用户代码中实现——它们是编译器 builtin。因此在使用时不会产生递归条件类型那样的堆栈深度问题。
自定义工具类型模式
以下是三个常用但并不内置的模式,展示如何组合基础工具类型构建领域级抽象。
DeepPartial — 递归可选
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
}
// 使用
interface Config {
server: { host: string; port: number }
database: { url: string; pool: number }
}
type PartialConfig = DeepPartial<Config>
// 现在 server 和 database 的内部字段也都是可选的了
const cfg: PartialConfig = { server: { host: "localhost" } } // ✅ OK
💡 最佳实践:
object会匹配数组为true,如果希望数组不被"深层拆解",加一层判定或在实用中限制递归边界。
RequiredKeys — 获取必选键
type RequiredKeys<T> = {
[K in keyof T]-?: {} extends Pick<T, K> ? never : K
}[keyof T]
// 使用
interface User {
id: number // 必选
name?: string // 可选
age?: number // 可选
}
type Mandatory = RequiredKeys<User>
// "id"
🔬 深入原理:
{} extends Pick<T, K>用于检测K对应的属性是否可选——只有可选属性的Pick<T, K>才可以将{}赋值给它。-?确保映射出来的候选值不自我变可选。最后[keyof T]将映射结果转为联合类型。
Brand Type — 名义类型模拟
type Brand<T, B> = T & { __brand: B }
type UserId = Brand<string, "UserId">
type OrderId = Brand<string, "OrderId">
function getUser(id: UserId) { /* ... */ }
const uid: UserId = "abc" as UserId // ✅ 需要显式断言
// getUser("abc") // ❌ string 不能赋值给 UserId
getUser(uid) // ✅ OK
💡 最佳实践:TypeScript 是结构化类型系统,
UserId在运行时仍然是普通string。__brand只在编译阶段参与类型检查,不会出现在运行时。这种模式被广泛用于防止 ID 混用(UserId vs OrderId)。
常见陷阱汇总
| 陷阱 | 说明 | 解法 |
|---|---|---|
Readonly<T> 浅层只读 |
嵌套对象内部属性仍可修改 | 自行实现 DeepReadonly,带递归映射 |
Omit<T, K> 不限制 K 在 T 内 |
传入不存在的键不会报错,Omit<User, "foo"> 返回完整 User |
显式加约束 Omit<T, K extends keyof T> 或用 key remapping |
Record<string, T> 值类型固定 |
所有值必须是 T 类型,不能混入其他类型 |
需要混合值时改用索引签名 { [key: string]: T | U } |
Exclude 分发行为 |
裸联合类型会被分发,但包装后的类型不会——Exclude<[1|2|3], [1]> 不会排除 1 |
确保 T 是裸联合类型,不要用元组或泛型包装 |
ReturnType<T> 对重载函数取最后签名 |
重载函数只能推断最后一个重载签名的返回类型 | 如果必须取特定重载的返回类型,单独声明函数类型而非合并 |
🚨 陷阱:
Pick<T, K>中K为联合类型时,结果类型仍然是对象类型。如果只需要单个属性的值类型,直接用索引访问T[K],而不是Pick<T, K>。