内置工具类型
TypeScript 自带的类型变换利器,拿来就用
🎯 学习目标
学完本章,你将能够:
- 能够熟练使用 Partial、Required、Readonly 转换属性可选性
- 能够使用 Pick、Omit、Record 选取或构造对象类型
- 能够运用 Parameters、ReturnType、Awaited 提取函数类型信息
- 能够理解 NoInfer 与 ThisParameterType 等高级工具类型
- 能够组合内置工具类型构建自定义工具类型解决实际问题
先看一个问题
更新用户信息时,所有字段都是可选的;创建用户时,id 不需要传。你每次都手写新类型吗?
interface User {
id: string;
name: string;
email: string;
age: number;
password: string;
createdAt: Date;
}
// 更新:所有字段可选 → 手写一遍?
interface UpdateUser {
id?: string; name?: string; email?: string;
age?: number; password?: string; createdAt?: Date;
}
// 创建:不要 id 和 createdAt → 又手写一遍?
interface CreateUser {
name: string; email: string;
age: number; password: string;
}
TypeScript 内置了十几个工具类型,专门解决这类问题——基于已有类型变换出新的类型。
Part 1: 属性修饰
Partial、Required、Readonly——修改属性的修饰状态
开篇那个问题你肯定遇到过:更新接口要所有字段可选,创建接口要排除 id。手写重复接口太痛苦了。TypeScript 内置了一组工具类型,专门解决这类问题。先从最常用的三个开始:让属性变可选、变必选、变只读。
Partial 全部可选
让所有属性变成可选
interface User {
name: string;
age: number;
email: string;
}
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string; }
// 实际应用:更新函数
function updateUser(user: User, updates: Partial<User>): User {
return { ...user, ...updates };
}
const user: User = { name: "张三", age: 25, email: "a@b.c" };
updateUser(user, { age: 26 }); // 只更新 age
updateUser(user, { name: "李四" }); // 只更新 name
updateUser(user, { name: "李四", email: "new@email.com" });
Required 与 Readonly
全部必选 / 全部只读
// Required——所有属性变必选(移除可选标记)
interface Config {
host: string;
port?: number;
timeout?: number;
}
type StrictConfig = Required<Config>;
// { host: string; port: number; timeout: number; }
// Readonly——所有属性变只读
type FrozenUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string; }
const frozen: FrozenUser = { name: "张三", age: 25, email: "a@b.c" };
// frozen.name = "李四"; // 报错!
// Readonly 也可以用于数组
const arr: ReadonlyArray<string> = ["a", "b"];
// arr.push("c"); // 报错!
ReadonlyArray 详解
只读数组的两种写法与使用场景
// 两种写法完全等价
const arr: readonly number[] = [1, 2, 3];
const arr2: ReadonlyArray<number> = [1, 2, 3];
// 都禁止修改
arr.push(4); // 报错!Property 'push' does not exist
arr[0] = 10; // 报错!Index signature only permits reading
// Readonly<T> vs readonly T[]
// Readonly 把所有属性变 readonly,但不影响嵌套数组的方法
type Obj = Readonly<{ items: number[] }>;
// { readonly items: number[] } items 的引用不可改,但内容可改!
// 元组也可以 readonly
const pair: readonly [string, number] = ["张三", 25];
// pair[0] = "李四"; // 报错!
常见误区:Readonly<T> 和 readonly T[] 不是一回事。前者让对象所有属性不可写,后者让数组不可变。对函数参数用 readonly 可以防止意外修改调用方的数据。
// 实际场景:函数参数不该被修改
function printNames(names: readonly string[]): void {
// names.push("新名字"); // 报错!安全
names.forEach(n => console.log(n));
}
const myNames = ["Alice", "Bob"];
printNames(myNames); // 调用方确信数据不会被改
Part 2: 属性筛选
Pick、Omit、Record——选取、排除、创建属性
属性修饰解决了"改状态"的问题,但还有一类需求:从接口里挑几个属性组成新类型,或者排除掉敏感字段。Pick 和 Omit 就是干这个的——一个"我要这几个",一个"我不要这几个"。Record 则是从零开始创建键值对类型。
Pick 选取属性
从类型中选取部分属性
interface User {
id: string;
name: string;
email: string;
age: number;
password: string;
createdAt: Date;
}
// API 返回给前端的公开信息——排除 password
type PublicUser = Pick<User, "id" | "name" | "email">;
// { id: string; name: string; email: string; }
// 列表项只需要名字和头像
type UserListItem = Pick<User, "id" | "name" | "age">;
// { id: string; name: string; age: number; }
Omit 排除属性
从类型中排除部分属性
// 排除敏感字段
type UserWithoutPassword = Omit<User, "password">;
// { id: string; name: string; email: string; age: number; createdAt: Date; }
// 排除多个属性
type UserBasic = Omit<User, "password" | "createdAt">;
// 实际应用:创建和更新 DTO
type CreateUserDTO = Omit<User, "id" | "createdAt">;
// 创建时不需要 id 和 createdAt
type UpdateUserDTO = Partial<Omit<User, "id" | "createdAt">>;
// 更新时所有字段可选,且不能改 id 和 createdAt
Pick 是"我要这几个",Omit 是"我不要这几个"。两者互补,通常选更短的那个。
Record 键值对
快速创建键值对类型
// Record——用联合类型作为键,统一值的类型
type Role = "admin" | "editor" | "viewer";
type Permissions = Record<Role, string[]>;
// { admin: string[]; editor: string[]; viewer: string[]; }
const perms: Permissions = {
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
// Record 的实现原理
type MyRecord<K extends string | number | symbol, V> = {
[key in K]: V;
};
Pick vs Omit
选择困难?看这张对比表
| 工具类型 | 思路 | 适合场景 | 示例 |
|---|---|---|---|
Pick<T, K> | 保留指定的 | 需要的属性少时 | Pick<User, "id" | "name"> |
Omit<T, K> | 排除指定的 | 不需要的属性少时 | Omit<User, "password"> |
属性多选少用 Pick,属性少排多用 Omit。两者可以组合使用:
// 组合使用
type CreateDTO = Partial<Omit<User, "id">>;
// 先排除 id,再全部变可选
Part 3: 联合操作
Exclude、Extract、NonNullable——操作联合类型的成员
前面两节操作的是对象接口的属性。如果操作对象是联合类型呢?Exclude 能从联合中去掉某些成员,Extract 能提取匹配的成员,NonNullable 专门去掉 null 和 undefined。它们和上一章学的条件类型、never 过滤是一脉相承的。
排除与提取
操作联合类型的成员
// Exclude——从联合类型中排除成员
type AllTypes = string | number | boolean | null;
type NoNull = Exclude<AllTypes, null>;
// string | number | boolean
type Status = "pending" | "active" | "closed" | "archived";
type ActiveStatus = Exclude<Status, "archived">;
// "pending" | "active" | "closed"
// Extract——从联合类型中提取成员
type StringOrNumber = Extract<AllTypes, string | number>;
// string | number
// NonNullable——排除 null 和 undefined
type MaybeString = string | null | undefined;
type DefiniteString = NonNullable<MaybeString>;
// string
实际应用
过滤数组中的空值
// NonNullable 的实际应用
function filterNull<T>(items: (T | null | undefined)[]): NonNullable<T>[] {
return items.filter((item): item is NonNullable<T> => item != null);
}
const mixed = ["a", null, "b", undefined, "c"];
const strings = filterNull(mixed); // string[]
// Exclude 的实际应用
type AllEvents = "click" | "focus" | "blur" | "scroll";
type KeyboardEvents = Exclude<AllEvents, "click" | "scroll">;
// "focus" | "blur"
Part 4: 函数操作
ReturnType、Parameters、Awaited——提取函数的类型信息
写高阶函数时,你经常需要"拿到某个函数的返回类型"或"拿到参数类型"——但不该手写,应该自动提取。ReturnType 和 Parameters 正是为此而生。再加上 Awaited 解包 Promise,异步函数的类型信息也能轻松获取。
返回类型与参数
从函数中提取类型信息
// ReturnType——获取函数的返回类型
function getUser() {
return { name: "张三", age: 25, email: "a@b.c" };
}
type UserFromFn = ReturnType<typeof getUser>;
// { name: string; age: number; email: string; }
// Parameters——获取函数的参数类型(元组)
function createUser(name: string, age: number, email: string) {
return { name, age, email };
}
type CreateUserParams = Parameters<typeof createUser>;
// [string, number, string]
// 使用参数类型创建包装函数
function withLog<T extends (...args: any[]) => any>(
fn: T
): (...args: Parameters<T>) => ReturnType<T> {
return (...args) => {
console.log("调用:", args);
return fn(...args);
};
}
const loggedCreate = withLog(createUser);
loggedCreate("张三", 25, "a@b.c"); // 自动提示参数
This 类型工具
ThisParameterType 与 OmitThisParameter
// ThisParameterType——提取函数的 this 参数类型
function greet(this: { name: string }) {
console.log(`Hello, ${this.name}`);
}
type ThisType = ThisParameterType<typeof greet>;
// { name: string }
// OmitThisParameter——返回去掉 this 参数的函数类型
type BoundGreet = OmitThisParameter<typeof greet>;
// () => void
// 实际场景:类型安全的事件绑定
function bindMethod<T, M extends keyof T>(
obj: T,
method: M
): OmitThisParameter<T[M] extends (...args: any[]) => any ? T[M] : never> {
return (obj[method] as Function).bind(obj) as any;
}
const logger = {
prefix: "[APP]",
log(this: { prefix: string }, msg: string) {
console.log(`${this.prefix} ${msg}`);
}
};
const boundLog = bindMethod(logger, "log");
boundLog("服务启动"); // [APP] 服务启动
ThisType<T> 是另一个相关工具,用于给对象字面量的方法提供共享的 this 上下文,常见于 Vue 的 Options API 类型定义中。
Awaited 解包
从 Promise 中提取内部类型
// Awaited——解包 Promise 的内部类型
async function fetchUsers() {
const res = await fetch("/api/users");
return res.json() as Promise<User[]>;
}
// 不用 Awaited:Promise<User[]>
// 用 Awaited:User[]
type UsersData = Awaited<ReturnType<typeof fetchUsers>>;
// User[]
// ConstructorParameters & InstanceType
class Person {
constructor(public name: string, public age: number) {}
}
type PersonArgs = ConstructorParameters<typeof Person>;
// [string, number]
type PersonInstance = InstanceType<typeof Person>;
// Person
构造器类型工具
ConstructorParameters 与 InstanceType
class User {
constructor(public name: string, public age: number) {}
greet() { return `Hi, I'm ${this.name}`; }
}
type UserParams = ConstructorParameters<typeof User>; // [string, number]
type UserInstance = InstanceType<typeof User>; // User
// 实际场景:泛型工厂函数
function create<T>(
Ctor: new (...args: any[]) => T,
...args: ConstructorParameters<typeof Ctor>
): T {
return new Ctor(...args);
}
const user = create(User, "张三", 25);
// 自动推断出 user: User
ConstructorParameters 只能用于 class 或 new (...args) => T 形式的构造签名,不能用在普通函数上。对普通函数请用 Parameters<typeof fn>。
// 进阶:依赖注入容器
type ServiceCtor<T> = new (...args: any[]) => T;
class Container {
private instances = new Map<ServiceCtor<any>, any>();
resolve<T>(Ctor: ServiceCtor<T>): InstanceType<ServiceCtor<T>> {
if (!this.instances.has(Ctor)) {
this.instances.set(Ctor, new Ctor());
}
return this.instances.get(Ctor);
}
}
const container = new Container();
const u = container.resolve(User); // u: User
Awaited 深入
不只是解包一层 Promise
// 嵌套 Promise 自动展平到最内层
type Deep = Awaited<Promise<Promise<string>>>;
// string —— 递归解包所有 Promise 层
// 结合泛型:提取异步函数的实际返回类型
async function getValue<T>(): Promise<T> { /* ... */ }
type ValueType = Awaited<ReturnType<typeof getValue<number>>>;
// number
// 实际场景:解包 API 响应类型
interface User { id: string; name: string; }
type ApiResponse<T> = Promise<{ data: T; error?: string }>;
type UserData = Awaited<ApiResponse<User>>["data"];
// User
// async function 的返回值自动 Awaited
async function fetchUser(id: string): Promise<User> {
const res = await fetch(`/api/users/${id}`);
return res.json();
}
// ReturnType 得到 Promise<User>
// 在 async 上下文中 TS 会自动 Awaited,所以实际类型是 User
Awaited 是 TypeScript 4.5 引入的。在此之前,ReturnType<typeof asyncFn> 返回的是 Promise<T> 而非 T,需要手动写 ReturnType<typeof asyncFn> extends Promise<infer U> ? U : never。现在直接用 Awaited 就行。
工具类型速查
一张表掌握所有内置工具类型
| 类型 | 作用 | 示例 |
|---|---|---|
Partial<T> | 所有属性变可选 | { name?: string } |
Required<T> | 所有属性变必选 | { name: string } |
Readonly<T> | 所有属性变只读 | { readonly name: string } |
Pick<T, K> | 选取部分属性 | { id: string; name: string } |
Omit<T, K> | 排除部分属性 | 排除 password |
Record<K, V> | 键值对类型 | { admin: string[] } |
Exclude<U, E> | 排除联合成员 | string | number |
Extract<U, E> | 提取联合成员 | string |
NonNullable<T> | 排除 null/undefined | string |
ReturnType<F> | 函数返回类型 | { name: string } |
Parameters<F> | 函数参数类型 | [string, number] |
Awaited<T> | 解包 Promise | User[] |
Part 5: 自定义扩展
内置的不够用?自己造
TypeScript 内置了十几个工具类型,覆盖了最常见的场景。但实际项目中你总会遇到"内置搞不定"的需求——比如递归可选、合并两个类型、提取非空属性名。别怕,前面学的映射类型和条件类型组合起来,什么都能造。这就是"类型编程"的实战。
自定义工具类型
DeepPartial、DeepReadonly 等
// DeepPartial——递归让所有属性可选
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
db: { host: string; port: number };
cache: { ttl: number; maxSize: number };
}
type PartialConfig = DeepPartial<Config>;
// { db?: { host?: string; port?: number }; ... }
// Merge——合并两个类型(后者覆盖前者)
type Merge<A, B> = Omit<A, keyof B> & B;
// NonNullableKeys——提取非空属性名
type NonNullableKeys<T> = {
[K in keyof T]: null extends T[K] ? never : K
}[keyof T];
interface User { name: string; nickname: string | null; age: number; }
type RequiredKeys = NonNullableKeys<User>; // "name" | "age"

图:Partial 与 DeepPartial 的递归深度
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 左侧 Partial:第一层虚线(可选),嵌套db:{host,port}保持实线(必填) - 右侧 DeepPartial:所有层级虚线(可选),host? port? 也是可选 - 底部:"Partial只处理第一层→嵌套对象内部仍是必填!" - 配色:必填实线深蓝,可选虚线橙色,嵌套层级渐变背景,16:9
组合模式
像搭积木一样构建复杂 DTO
interface User {
id: string;
name: string;
email: string;
password: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
updatedAt: Date;
}
// 组合1:创建 DTO——排除自动生成的字段
type CreateUserDTO = Omit<User, "id" | "createdAt" | "updatedAt">;
// 组合2:更新 DTO——部分可选 + 排除不可变字段
type UpdateUserDTO = Partial<Omit<User, "id" | "createdAt">>;
// 组合3:API 响应——公开信息 + 额外字段
type UserResponse = Pick<User, "id" | "name" | "email" | "role"> & {
postCount: number;
};
// 组合4:列表项——精简 + 关联计数
type UserListItem = Pick<User, "id" | "name" | "role"> & {
avatar: string;
};
工具类型的真正威力在于组合。单个工具类型做一件事,组合起来就能覆盖 90% 的 DTO 场景——不用手写重复的接口。

图:工具类型组合管线
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 左侧:User { id, name, email, password, role, createdAt } 完整卡片 - 管线第一站:Omit<"password"> → 去掉password(删除线) - 管线分叉:Omit<"id"|"createdAt">→CreateUserDTO | Partial→UpdateUserDTO(虚线边框) | Pick<"id"|"name">→PublicUser - 配色:浅灰背景,原始深蓝边框,DTO暖色系,箭头橙色,16:9
高级组合模式
用工具类型构建完整的 CRUD DTO 管线
// 定义基础实体
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface Article extends BaseEntity {
title: string;
content: string;
authorId: string;
tags: string[];
}
// 通用 DTO 工厂——任何实体都能复用
type CreateDTO<T extends BaseEntity> = Omit<T, "id" | "createdAt" | "updatedAt">;
type UpdateDTO<T extends BaseEntity> = Partial<CreateDTO<T>>;
type PublicDTO<T, K extends keyof T = keyof T> = Pick<T, K>;
// 具体使用
type CreateArticle = CreateDTO<Article>;
// { title: string; content: string; authorId: string; tags: string[] }
type UpdateArticle = UpdateDTO<Article>;
// { title?: string; content?: string; authorId?: string; tags?: string[] }
type ArticlePublic = PublicDTO<Article, "id" | "title" | "tags">;
// { id: string; title: string; tags: string[] }
// 条件组合:根据字段类型做不同处理
type TrimmedString<T> = T extends string ? string : T;
type StrictUpdate<T> = {
[K in keyof T]?: T[K] extends string ? string : T[K];
};
// 组合验证:标记必填字段
// 注意:类型参数不要命名为 Required,它会遮蔽同名的内置工具类型 Required,
// 导致 TS2315 报错。改用 ReqKeys 这类描述性名字。
type WithValidation<T, ReqKeys extends keyof T> =
Required<Pick<T, ReqKeys>> & Partial<Omit<T, ReqKeys>>;
type ArticleSubmit = WithValidation<CreateDTO<Article>, "title" | "content">;
// title 和 content 必填,其余可选
组合嵌套不要太深。如果 Partial<Omit<Pick<T, ...>, ...>> 超过三层,考虑定义中间类型别名提高可读性。
分类总览
四大类工具类型,各司其职

图:内置工具类型四大分类
【AI 生图提示词】请生成一张中文技术示意图,风格简洁、配色柔和、适合教学: - 画面主体:树形分类图,顶部为"TypeScript 内置工具类型" - 四大分支,每分支用不同颜色: - 属性修饰类(蓝色):Partial、Required、Readonly——修改属性的可选性和可写性 - 属性筛选类(绿色):Pick、Omit、Record——选取、排除或创建属性 - 联合操作类(橙色):Exclude、Extract、NonNullable——操作联合类型的成员 - 函数操作类(紫色):ReturnType、Parameters、Awaited、ConstructorParameters、InstanceType——提取函数的类型信息 - 配色建议:浅灰背景,四大类用四种不同暖色,子项用浅色卡片 - 画面比例:16:9 - 避免:过度装饰、3D 效果、杂乱的颜色
记住这四个分类,遇到类型变换需求时先想"该用哪一类",再从中选具体的工具类型。
NoInfer
TS 5.4+ 新增,阻止推断到不该推断的地方
// 问题:createAPI 的 baseUrl 被推断为传入的字符串字面量
function createAPI<T extends string>(baseUrl: T, routes: T[]): T {
return baseUrl;
}
// TS 推断 baseUrl 为 "/api/v1"(字面量类型)
// 而我们希望 baseUrl 只是被约束,不被推断
// 解决:用 NoInfer 包裹不想被推断的参数
function createAPI<T extends string>(
baseUrl: NoInfer<T>, // 不会从这里推断 T
routes: T[] // 从这里推断 T
): T { return baseUrl; }
// 现在 T 只从 routes 推断
createAPI("/api/v1", ["/users", "/posts"]); // OK
简单理解:NoInfer<T> 告诉 TS "这个位置不要参与推断"。适合 API 函数中有多个泛型参数、你想精确控制推断来源的场景。
博客系统练习
练习
任务:基于下面的 Post 接口,用工具类型组合出 5 个 DTO。
interface Post {
id: string;
title: string;
content: string;
authorId: string;
tags: string[];
status: "draft" | "published" | "archived";
createdAt: Date;
updatedAt: Date;
}
// 1. CreatePostDTO: 创建文章,不含自动生成的字段
// 2. UpdatePostDTO: 更新文章,所有字段可选,不能改 id
// 3. PublicPost: 公开文章,排除 authorId
// 4. PostListItem: 列表项,只要 id、title、tags、status
// 5. PostDetail: 详情页,PublicPost + authorName
// 你的代码:
参考答案
type CreatePostDTO = Omit<Post, "id" | "createdAt" | "updatedAt">;
type UpdatePostDTO = Partial<Omit<Post, "id" | "createdAt" | "updatedAt">>;
type PublicPost = Omit<Post, "authorId">;
type PostListItem = Pick<Post, "id" | "title" | "tags" | "status">;
type PostDetail = PublicPost & { authorName: string };
性能考量
大型项目中的工具类型注意事项
// 问题:深层递归类型会让编译器变慢
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object
? DeepPartial<T[K]> // 每层递归都增加编译开销
: T[K];
};
// 如果 T 有 10+ 层嵌套,编译时间会明显增长
// 排查慢类型:用 tsc --diagnostics
// $ tsc --diagnostics
// Files: 120
// Types: 8500 <-- 数值异常大?检查是否有过度复杂的类型
// Instantiations: 50000 <-- 超过 10 万就该优化了
| 优化策略 | 说明 |
|---|---|
| 优先用内置工具类型 | 内置类型使用 intrinsic 编译器魔法,比手写实现快得多 |
| 限制递归深度 | 给递归类型加上深度计数器,超过 5-7 层就停止 |
| 用 interface 替代复杂别名 | interface 的缓存机制比 type 更好 |
| 拆分大类型 | 把巨型交叉类型拆成多个中间别名 |
内置工具类型(如 Partial、Readonly)底层有些使用了 intrinsic 关键字——这是编译器内置实现,比我们手写的映射类型快得多。所以"能用内置就用内置"不只是风格问题,还有性能收益。
常见问题
问题一:Partial 只做浅层可选
interface Config {
db: { host: string; port: number };
}
type P = Partial<Config>;
// { db?: { host: string; port: number } }
// db 是可选的,但 db 里面的属性不是可选的!
解决:需要递归可选时,用 DeepPartial。
问题二:ReturnType 对泛型函数效果有限
function foo<T>(val: T): T { return val; }
type R = ReturnType<typeof foo>; // unknown(泛型未实例化)
解决:在具体使用场景下获取返回类型,而非对泛型定义直接使用。
试一试
练习一
任务:给定 User 接口,用工具类型创建一个"注册 DTO"(不要 id 和 createdAt)和一个"公开信息"(只要 id、name、email)。
interface User {
id: string; name: string; email: string;
password: string; createdAt: Date;
}
// 你的代码:
参考答案
type RegisterDTO = Omit<User, "id" | "createdAt">;
// { name: string; email: string; password: string }
type PublicInfo = Pick<User, "id" | "name" | "email">;
// { id: string; name: string; email: string }
练习二
任务:写一个 Merge<A, B> 工具类型,让 B 的属性覆盖 A 中同名的属性。
type Result = Merge<{ a: string; b: number }, { b: boolean }>;
// 期望:{ a: string; b: boolean }
参考答案
type Merge<A, B> = Omit<A, keyof B> & B;
// 先排除 A 中与 B 同名的属性,再与 B 交叉
📝 要点总结
本章核心知识点
属性转换
Partial 全可选、Required 全必选、Readonly 全只读
选取与构造
Pick 选取属性、Omit 排除属性、Record 构造键值对
函数类型工具
Parameters 提取参数、ReturnType 提取返回、Awaited 解包 Promise
高级工具
NoInfer 阻止推断、ThisType 标注 this、构造器类型
自定义组合
组合多个工具类型,构建 DeepPartial、Mutable 等
下一章
工具类型让你从已有类型变换出新类型。下一章我们学习 TypeScript 中的类——给 OOP 加上类型保护。