Skip to content
工具类型

工具类型

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 : never

NonNullable<T>

定义:从类型 T 中排除 nullundefined

语法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> 只能去除 nullundefined,不能去除 voidnever 或其他 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 : never

ConstructorParameters<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 : never

InstanceType<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 : never

ThisParameterType<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 : unknown

OmitThisParameter<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 是否具有符合 PromiseLikethen 方法,如果是就通过 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>