内置工具类型

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/undefinedstring
ReturnType<F>函数返回类型{ name: string }
Parameters<F>函数参数类型[string, number]
Awaited<T>解包 PromiseUser[]

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 的递归深度

组合模式

像搭积木一样构建复杂 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 场景——不用手写重复的接口。

图:工具类型组合管线

高级组合模式

用工具类型构建完整的 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, ...>, ...>> 超过三层,考虑定义中间类型别名提高可读性。

分类总览

四大类工具类型,各司其职

图:内置工具类型四大分类

记住这四个分类,遇到类型变换需求时先想"该用哪一类",再从中选具体的工具类型。

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 加上类型保护。

1 / X