不喜欢广告? 无广告 今天

TypeScript 工具类型——值得记忆的那些类型(以及其他的类型)

更新于

部分、选择、省略、记录、返回类型、参数、等待——八个实用类型,包含前后示例以及对哪些类型你每周都会实际使用的诚恳看法。

TypeScript 工具类型 — 值得记忆的那些(以及其他的)1
广告 移除?

您可能已经读过一篇 TypeScript 文档,其中按字母顺序列出了 20 多个工具类型,每个类型仅用一句话定义,让您困惑于哪些类型值得掌握,哪些只是为框架内部开发人员准备的。这是更真实的情况:八个类型,每个类型都配有明确的前后示例,并明确指出日常使用与偶尔使用的区别。

如果您从原始 JSON 开始,尚未编写接口定义,那么 JSON 转 TypeScript 转换器 可以几秒钟内帮您生成基础接口,然后回到这里,将这些工具类型叠加到接口上。

Partial — 每周使用。优先学习。

使 T 中的所有属性变为可选。典型用例是类似 PATCH 的更新操作,您只发送发生变更的字段。

// Before: manually duplicating the interface with optional versions of everything
interface UserUpdate {
  name?: string;
  email?: string;
  age?: number;
}

// After: Partial derives it from the source of truth
interface User {
  name: string;
  email: string;
  age: number;
}

function updateUser(id: string, updates: Partial<User>) {
  // updates.name is string | undefined — TypeScript knows
}

“之前”版本的问题在于:如果您在 User中添加一个字段,就必须记得在 UserUpdate 中也添加它。而使用 Partial<User>后,只要在源接口中添加字段,更新操作中就会自动可用,类型始终保持同步。

Required — 每月使用,但在一个特定场景下非常有用。

Partial 的反面——移除类型中的所有 ? 。在验证之后最常用:您收到一个松散类型的配置对象,验证所有字段都存在,然后希望 TypeScript 停止建议某些字段可能是未定义的。

// Before: after validating, you still fight undefined checks everywhere
interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}

function initApp(config: Config) {
  config.apiUrl?.trim(); // need optional chaining even after checking
}

// After: Required signals "this has been validated, stop warning me"
function validateAndInit(raw: Config): void {
  if (!raw.apiUrl) throw new Error("apiUrl required");
  if (!raw.timeout) throw new Error("timeout required");
  if (!raw.retries) throw new Error("retries required");

  const config = raw as Required<Config>;
  config.apiUrl.trim(); // no optional chaining needed
}

诚恳的说明: Required<T> 不进行运行时验证——您仍需编写检查代码。它只是改变了 TypeScript 在下游的信念。使用它来在类型层面传达“此数据已验证”的信息。

Pick — 每周使用。指定您想要的属性。

创建一个只包含从 T中命名属性的新类型。

// Before: manually maintaining a separate interface that drifts from User
interface UserPublicProfile {
  name: string;
  bio: string;
  avatarUrl: string;
}

// After: Pick stays in sync with the source
interface User {
  id: string;
  name: string;
  email: string;       // private — shouldn't go to clients
  passwordHash: string;  // definitely private
  bio: string;
  avatarUrl: string;
}

type UserPublicProfile = Pick<User, "name" | "bio" | "avatarUrl">;

Pick 是当您想要的属性列表 比您需要的属性列表 短时的正确工具。当情况相反时,应选择 Omit。 excludeOmit — 每周使用。指定您不想要的属性。

是 Pick 的补充——创建一个包含所有属性的类型,但排除您命名的属性。当您有一个大型接口,只需排除少数字段时,Omit 比列出所有想要的字段更清晰地表达了意图。

Pick 和 Omit 都不做的一个事情:改变单个属性的类型。如果您希望选择字段并 T 转换其类型,那么您需要的是映射类型,这是一种完全不同的工具。

// Before: manually listing every field except the sensitive ones
// Easy to miss a new field added to User later
interface UserForClient {
  id: string;
  name: string;
  email: string;
  bio: string;
  avatarUrl: string;
  // passwordHash intentionally omitted — but a future dev might not know that
}

// After: Omit states the exclusion explicitly
type UserForClient = Omit<User, "passwordHash">;
// If User gains a new "address" field, UserForClient gets it automatically

Record — 每周使用。带类型的字典。 创建一个对象类型,其所有键的类型为

,所有值的类型为

。真正的好处出现在 K 是联合类型时——TypeScript 会在您遗漏键时报错。通用索引签名会默默接受不完整的对象;而使用联合键的 Record 不会。 V穷尽性检查是真正的优势。当键代表有意义的内容,如角色、事件类型、HTTP 方法或状态码时,缺失一个情况就是错误,而不是简单的遗漏。 K ReturnType — 如果您使用第三方函数,每周都会用到。

// Before: index signature with no exhaustiveness checking
const rolePermissions: { [key: string]: string[] } = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  // forgot "viewer" — TypeScript doesn't notice
};

// After: Record with a union key enforces completeness
type Role = "admin" | "editor" | "viewer";

const rolePermissions: Record<Role, string[]> = {
  admin: ["read", "write", "delete"],
  editor: ["read", "write"],
  viewer: ["read"],
  // TypeScript error if any Role is missing
  // TypeScript error if you try to add an unknown key
};

提取函数的返回类型。当您消费一个库函数并希望为存储其输出的变量定义类型时最实用——无需单独导入返回类型(因为库并不总是清晰地导出返回类型)。

Parameters — 偶尔使用。主要用于包装函数。

提取函数的参数类型作为元组。诚恳地说:除非您编写大量包装函数或高阶工具,否则您不会每周都用到它。但当您需要它时,没有更干净的替代方案。

// Before: manually annotating the return type — it can drift
function getCurrentUser() {
  return { id: "u1", name: "Alice", roles: ["admin"] as const };
}

let cachedUser: { id: string; name: string; roles: readonly string[] };
// If getCurrentUser's return changes, this annotation doesn't update automatically

// After: ReturnType derives it and keeps in sync
type CurrentUser = ReturnType<typeof getCurrentUser>;
let cachedUser: CurrentUser;

// Especially useful with unexported library types:
import { useQuery } from "@tanstack/react-query";
type QueryResult = ReturnType<typeof useQuery>;
// No need to figure out what QueryObserverResult<...> looks like or import it

展开 + 剩余模式(

)是主要用法。它也出现在测试模拟中:

// Before: manually duplicating parameter types in the wrapper
function createEvent(
  type: string,
  payload: Record<string, unknown>,
  timestamp: number
) { /* ... */ }

// Every time createEvent changes, this wrapper needs a manual update:
function loggedCreateEvent(
  type: string,
  payload: Record<string, unknown>,
  timestamp: number
) {
  console.log("Creating event:", type);
  return createEvent(type, payload, timestamp);
}

// After: Parameters keeps the wrapper in sync automatically
function loggedCreateEvent(...args: Parameters<typeof createEvent>) {
  console.log("Creating event:", args[0]);
  return createEvent(...args);
}

为您提供一个完全类型化的模拟,而无需单独导入函数的签名。...args: Parameters<F>Awaited — 当您需要展开 Promise 类型而无需调用函数时使用。 jest.fn<ReturnType<F>, Parameters<F>>() 在 TypeScript 4.5(2021 年 11 月发布)中新增。递归展开

以获取

。在它出现之前,获取异步函数的解析类型需要编写条件类型: Promise<T> ——这在双重包装的 Promise 上会失效,且在差异对比中难以阅读。 T组合 ReturnType<F> extends Promise<infer U> ? U : never 值得记住作为一个整体——它覆盖了应用代码中 90% 的异步类型提取需求。

// Before: conditional type to unwrap — breaks on nested Promises
async function fetchUser(id: string) {
  const res = await fetch(`/api/users/${id}`);
  return res.json() as { id: string; name: string; email: string };
}

type FetchedUser =
  ReturnType<typeof fetchUser> extends Promise<infer U> ? U : never;
// Works for Promise<T> but not Promise<Promise<T>>

// After: Awaited handles both cases cleanly
type FetchedUser = Awaited<ReturnType<typeof fetchUser>>;

// Awaited recursively unwraps:
type A = Awaited<Promise<string>>;            // string
type B = Awaited<Promise<Promise<string>>>;  // string
type C = Awaited<string>;                      // string (no-op on non-Promise)

速查表 Awaited<ReturnType<typeof fn>> 工具类型

使用频率

主要用途作用所有属性变为可选PATCH/更新负载
Partial<T>所有可选属性变为必需每周验证后的“已完成”信号
Required<T>仅保留命名属性每月公共 API 形状、视图模型
Pick<T, K>移除命名属性每周剥离敏感或无关字段
Omit<T, K>键类型为 K,值类型为 V 的对象每周带穷尽性检查的带类型字典
Record<K, V>提取函数的返回类型每周每周使用(尤其在使用库时)
ReturnType<F>为从推断或未导出的返回类型中定义变量提取函数的参数类型作为元组偶尔使用
Parameters<F>包装函数、带类型模拟递归展开 Promise 类型获取异步函数的解析类型
Awaited<T>列表中未包含的类型递归展开 Promise 类型TypeScript 还提供了

以及几种模板字面量类型。它们是真实存在的,偶尔有用,但主要出现在库代码、通用工具和类型级元编程中,而不是在典型的应用逻辑中。如果您已经记住了上述八个类型,当您真正需要时再查找其余类型。

所有类型背后的原理是相同的:描述一个现有类型的转换,而不是复制它。具体的工具类型名称只是表达这种意图的词汇,用于让 TypeScript 理解您的意图。 Readonly<T>, NonNullable<T>, Extract<T,U>, Exclude<T,U>, InstanceType<C>, ConstructorParameters<C>,以及几种模板字面量类型。它们是真实的,偶尔有用,但主要出现在库代码、泛型工具和类型级元编程中,而不是在典型的业务逻辑中。如果你已经记住了上面提到的八种,当真正需要时再去查找其余的。

它们背后的原理在所有类型中都是相同的:描述对现有类型的转换,而不是复制它。具体的工具类型名称只是在 TypeScript 中表达这种意图的词汇。

想要享受无广告的体验吗? 立即无广告

安装我们的扩展

将 IO 工具添加到您最喜欢的浏览器,以便即时访问和更快地搜索

添加 Chrome 扩展程序 添加 边缘延伸 添加 Firefox 扩展 添加 Opera 扩展

记分板已到达!

记分板 是一种有趣的跟踪您游戏的方式,所有数据都存储在您的浏览器中。更多功能即将推出!

广告 移除?
广告 移除?
广告 移除?

新闻角 包含技术亮点

参与其中

帮助我们继续提供有价值的免费工具

给我买杯咖啡
广告 移除?