首页 / TypeScript 入门教程 / 内置工具类型(上)

TypeScript 入门教程

内置工具类型(上)

本教程共 80 篇 · 第 41 篇 · 更新于 2026-08-10 · 约 10 分钟阅读

TypeScriptTypeScript 入门教程工具类型泛型

本节目标:掌握五个高频工具类型——PartialRequiredReadonlyPickOmit——不仅会用,还能自己”手写”出来,理解它们不是魔法。

TypeScript 内置了一批工具类型(Utility Types),全部定义在 lib.es5.d.ts 里,不需要额外 import 就能用。它们本质上都是泛型 + 映射类型的组合。看懂实现原理,你就真正理解了 TypeScript 类型系统的表达能力。

这一章先讲最常用的五个。

Partial<Type>:全变可选

它能干什么

Partial<Type> 接收一个对象类型,返回一个所有属性都变成可选的新类型:

interface User {
  id: number;
  name: string;
  email: string;
}

// 全部属性变成了可选的
type PartialUser = Partial<User>;

// 等价于:
// {
//   id?: number;
//   name?: string;
//   email?: string;
// }

典型使用场景:更新操作。你只想改某几个字段,其他字段不传:

function updateUser(id: number, fields: Partial<User>): void {
  // fields 里每个属性都是可选的,只传要改的就行
}

updateUser(1, { name: "李四" });           // ✅ 只传 name
updateUser(1, { name: "李四", email: "" }); // ✅ 传一部分

手写实现原理

type MyPartial<T> = {
  [K in keyof T]?: T[K];
};

逐行解释:

  • keyof T:拿到 T 的所有键名组成的联合类型。
  • [K in keyof T]:遍历每个键 K
  • ?: T[K]:把每个属性变成可选(?),值类型保持不变(T[K])。

就这一行代码——这就是 Partial 的全部魔法。它本质是一个映射类型,遍历源类型的所有键,给每个字段加了一个问号。

Required<Type>:全变必选

它能干什么

Required<Type>Partial 的反向操作——把所有可选属性变成必选:

interface Config {
  host?: string;
  port?: number;
  debug?: boolean;
}

// 全部属性变成必选
type StrictConfig = Required<Config>;

// 等价于:
// {
//   host: string;
//   port: number;
//   debug: boolean;
// }

// ❌ 必须提供所有属性
const config: StrictConfig = {
  host: "localhost",
  port: 3000,
  debug: true,
};

手写实现原理

type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

注意这里的 -?——它是映射类型的修饰符操作。? 表示加可选,-? 表示移除可选。对于本来就必选的属性,-? 没有影响。对于可选的属性,-? 把问号去掉,变成必选。

Readonly<Type>:全变只读

它能干什么

Readonly<Type> 把对象所有属性变成只读——创建后不能修改:

interface Todo {
  title: string;
  completed: boolean;
}

const todo: Readonly<Todo> = {
  title: "写完这篇教程",
  completed: false,
};

// todo.title = "改个标题";  // ❌ 不能赋值给只读属性

一个很实用的场景:确保配置对象在运行时不被人篡改:

function init(config: Readonly<{ env: string; debug: boolean }>) {
  // config.env = "production"; // ❌ 安全——编译期就拦住了
  if (config.debug) {
    console.log("调试模式开启");
  }
}

手写实现原理

type MyReadonly<T> = {
  readonly [K in keyof T]: T[K];
};

Partial 几乎一样,只是 ? 换成了 readonlyreadonly 是映射类型的另一个修饰符。

Tip

如果想把 readonly 去掉(“解锁”一个类型),用 -readonly

type MyMutable<T> = {
  -readonly [K in keyof T]: T[K];
};

Pick<Type, Keys>:选取指定键

它能干什么

Pick<Type, Keys>Type 中挑选 Keys 指定的属性,构造一个新类型:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 只暴露公开信息,不暴露密码
type PublicUser = Pick<User, "id" | "name" | "email">;

// 等价于:
// {
//   id: number;
//   name: string;
//   email: string;
// }

function getPublicProfile(user: User): PublicUser {
  return {
    id: user.id,
    name: user.name,
    email: user.email,
  };
}

手写实现原理

type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};

注意 K extends keyof T——这个约束确保你只能选 T 上存在的键名。如果你传一个不存在的键,TypeScript 会在编译期报错:

// ❌ "avatar" 不是 User 的属性
// type Bad = Pick<User, "avatar">;

跟前面几个工具类型不同,Pick 的映射遍历的不是 keyof T,而是 K——也就是你指定的键名联合。

Omit<Type, Keys>:排除指定键

它能干什么

Omit<Type, Keys>Type 中排除 Keys 指定的属性,剩下的组成新类型。跟 Pick 思路相反:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// 排除敏感字段
type SafeUser = Omit<User, "password">;

// 等价于:
// {
//   id: number;
//   name: string;
//   email: string;
// }

// 也可以排除多个
type UserPreview = Omit<User, "password" | "email">;
// 等价于:{ id: number; name: string; }

手写实现原理

type MyOmit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};

Omit 比前面几个稍微”复合”一点——它借用了 Exclude 来过滤键名:

  • keyof TT 的所有键。
  • Exclude<keyof T, K>:从所有键中排除 K
  • [P in Exclude<keyof T, K>]:只遍历剩下的键。

Exclude 是另一个内置工具类型(下一章讲),它的作用是”从联合类型中排除某些成员”。Omit 本质上就是 Pick 的”取反”——挑出不在 K 中的键。

五个工具类型对比

工具类型作用修饰符
Partial<T>全部属性变可选+?
Required<T>全部属性变必选-?
Readonly<T>全部属性变只读+readonly
Pick<T, K>选取指定键
Omit<T, K>排除指定键无(结合 Exclude)

小结

这五个工具类型的核心是映射类型语法 [K in keyof T]: ...。Partial/Required/Readonly 是”遍历所有属性 + 加修饰符”,Pick/Omit 是”挑出或排除部分属性”。理解它们的实现方式之后,你可以自己写专属的工具类型——比如”把一个类型的部分属性变成可选”或者”只保留字符串类型的属性”。

这些不是黑魔法,就是一行或两行泛型映射类型。下一章我们继续看进阶工具类型——它们会用到条件类型,表达能力更上一层楼。