TypeScript 工具类型——值得记忆的那些类型(以及其他的类型)
部分、选择、省略、记录、返回类型、参数、等待——八个实用类型,包含前后示例以及对哪些类型你每周都会实际使用的诚恳看法。
您可能已经读过一篇 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 中表达这种意图的词汇。
