TypeScript 工具类型——值得记忆的那些类型(以及其他的类型)
部分、选择、省略、记录、返回类型、参数、等待——八个实用类型,包含前后示例以及对哪些类型你每周都会实际使用的诚恳看法。
You’ve probably read a TypeScript docs page that lists 20+ utility types in alphabetical order, defined each one in a sentence, and left you wondering which ones to actually internalize versus which ones exist for people writing framework internals. This is the more honest version: eight types, concrete before/after examples, and a straight answer on daily use versus occasionally useful.
If you’re starting from raw JSON and haven’t written your interfaces yet, the JSON to TypeScript converter will get you to a base interface in seconds — then come back here to layer these utility types on top.
Partial<T> — Weekly use. Learn it first.
Makes every property in T optional. The canonical use case is PATCH-style updates, where you only send the fields that changed.
// 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
}
The problem with the “Before” version: if you add a field to User, you have to remember to add it to UserUpdate too. With Partial<User>, adding a field to the source interface automatically makes it available in updates. The types stay in sync.
Required<T> — Monthly use, but saves you in one specific scenario.
The inverse of Partial — strips all ? from a type. Most useful after validation: you receive a loosely-typed config object, validate that all fields are present, and then want TypeScript to stop suggesting that everything might be undefined.
// 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
}
The honest note: Required<T> doesn’t do runtime validation — you still need to write the checks. It just changes what TypeScript believes downstream. Use it to communicate “this has been validated” at the type level.
Pick<T, K> — Weekly use. Name what you want.
Creates a new type containing only the properties you name from T. Useful when you want a subset of a larger interface — public API shapes, view models, serialized responses.
// 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 is the right tool when the list of properties you want is shorter than the list you’d need to exclude. When it flips the other way, reach for Omit.
Omit<T, K> — Weekly use. Name what you don’t want.
Complement of Pick — creates a type with all properties of T except the ones you name. When you have a large interface and only a few fields to exclude, Omit makes the intent clearer than listing everything you do want.
// 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
One thing neither Pick nor Omit does: change the type of individual properties. If you want to pick fields 且 transform their types, you’re looking at a mapped type, which is a different tool entirely.
Record<K, V> — Weekly use. The typed dictionary.
Creates an object type where all keys are type K and all values are type V. The real win comes when K is a union type — TypeScript will error if you’re missing a key. Generic index signatures silently accept incomplete objects; Record with a union key doesn’t.
// 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
};
The exhaustiveness check is the real win. It matters when the keys represent something meaningful like roles, event types, HTTP methods, or status codes — anything where a missing case is a bug, not just an omission.
ReturnType<F> — Weekly use if you consume third-party functions.
Extracts the return type of a function. Most useful when consuming a library function and wanting to type a variable that holds its output — without having to import the return type separately (which libraries don’t always export cleanly).
// 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
Parameters<F> — Occasional use. Mostly for wrappers.
Extracts the parameter types of a function as a tuple. Honest take: you won’t reach for this weekly unless you write a lot of wrapper functions or higher-order utilities. But when you need it, there’s no clean alternative.
// 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);
}
The spread + rest pattern (...args: Parameters<F>) is the main idiom. It also shows up in test mocks: jest.fn<ReturnType<F>, Parameters<F>>() gives you a fully typed mock without importing the function’s signature separately.
Awaited<T> — Use when you need to unwrap Promise types without calling them.
Added in TypeScript 4.5 (released November 2021). Recursively unwraps Promise<T> to get at T. Before it existed, getting the resolved type of an async function meant writing a conditional type: ReturnType<F> extends Promise<infer U> ? U : never — which breaks on double-wrapped Promises and is unpleasant to read in diffs.
// 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)
The combination Awaited<ReturnType<typeof fn>> is worth memorizing as a unit — it covers 90% of async type extraction in app code.
Cheatsheet
| Utility type | 作用 | Use frequency | Primary use case |
|---|---|---|---|
Partial<T> | All properties become optional | 每周 | PATCH/update payloads |
Required<T> | All optional properties become required | 每月 | Post-validation “this is complete” signal |
Pick<T, K> | Keeps only named properties | 每周 | Public API shapes, view models |
Omit<T, K> | Removes named properties | 每周 | Stripping sensitive or irrelevant fields |
Record<K, V> | Object with keys of type K, values of type V | 每周 | Typed dictionaries with exhaustiveness checking |
ReturnType<F> | Extracts function’s return type | Weekly (especially with libraries) | Type a variable from an inferred or unexported return type |
Parameters<F> | Extracts function’s parameter types as a tuple | Occasional | Wrapper functions, typed mocks |
Awaited<T> | Recursively unwraps Promise types | Occasional | Getting resolved type from async functions |
The ones not on this list
TypeScript also ships Readonly<T>, NonNullable<T>, Extract<T,U>, Exclude<T,U>, InstanceType<C>, ConstructorParameters<C>, and several template literal types. They’re real and occasionally useful, but they come up in library code, generic utilities, and type-level metaprogramming — not in typical application logic. If you’ve memorized the eight above, look up the rest when you actually need them.
The underlying principle is the same across all of them: describe a transformation of an existing type rather than duplicating it. The specific utility type names are just vocabulary for expressing that intent to TypeScript.
