TypeScript 的标准库带了几十个 utility type。熟练用能少写一半重复定义。
下面只挑实战频率最高的 20+ 个 + 5 个我自己常造的。
内置常用
Partial<T> / Required<T>
interface User { id: string; name: string; email: string }
// 所有字段可选(PATCH 接口最常用)
function update(id: string, patch: Partial<User>) {}
update('1', { name: 'Alice' }) // OK
// 所有字段必选
type FullUser = Required<Partial<User>>
Pick<T, K> / Omit<T, K>
type UserPublic = Pick<User, 'id' | 'name'> // 只要这些字段
type UserPrivate = Omit<User, 'email' | 'password'> // 去掉这些字段
Record<K, V>
type StatusMap = Record<'pending' | 'done' | 'failed', number>
// { pending: number; done: number; failed: number }
type Cache<T> = Record<string, T> // 字典
Readonly<T> / ReadonlyArray<T>
function process(items: ReadonlyArray<string>) {
items.push('x') // ❌ 编译错
}
ReturnType<F> / Parameters<F>
function fetchUser(id: string): Promise<User> { ... }
type UserResp = Awaited<ReturnType<typeof fetchUser>> // User
type Args = Parameters<typeof fetchUser> // [string]
Awaited<T> 解 Promise 包装。
Exclude<T, U> / Extract<T, U>
type Color = 'red' | 'green' | 'blue' | 'transparent'
type Opaque = Exclude<Color, 'transparent'> // 'red' | 'green' | 'blue'
type Warm = Extract<Color, 'red' | 'orange'> // 'red'
NonNullable<T>
type StringOrNull = string | null | undefined
type DefinitelyString = NonNullable<StringOrNull> // string
Awaited<T>
type A = Awaited<Promise<Promise<number>>> // number
递归解包 Promise。
自定义常用
DeepPartial<T> — 递归 Partial
type DeepPartial<T> = T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T
type Config = { a: { b: { c: number } } }
type PartialConfig = DeepPartial<Config>
// { a?: { b?: { c?: number } } }
适合 patch 嵌套对象。
DeepReadonly<T>
type DeepReadonly<T> = T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T
PickRequired<T, K> — 指定字段必选,其它保持
type PickRequired<T, K extends keyof T> = T & { [P in K]-?: T[P] }
interface Opts { name?: string; age?: number; email?: string }
type WithName = PickRequired<Opts, 'name'>
// { name: string; age?: number; email?: string }
-? 是 "去掉 optional 修饰"。
Branded<T, B> — 名义类型(防混淆)
type Branded<T, B extends string> = T & { __brand: B }
type UserId = Branded<string, 'UserId'>
type PostId = Branded<string, 'PostId'>
function getUser(id: UserId): User { ... }
const uid = 'u1' as UserId
const pid = 'p1' as PostId
getUser(uid) // OK
getUser(pid) // ❌ Type 'PostId' is not assignable to 'UserId'
让基础类型(string / number)按业务含义区分,编译期阻止误用。
ValueOf<T>
type ValueOf<T> = T[keyof T]
const Status = { Active: 'active', Inactive: 'inactive' } as const
type StatusVal = ValueOf<typeof Status> // 'active' | 'inactive'
Mutable<T> — 反向 Readonly
type Mutable<T> = { -readonly [K in keyof T]: T[K] }
XOR<T, U> — 两选一不能同时
type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never }
type XOR<T, U> = (Without<T, U> & U) | (Without<U, T> & T)
type Login = XOR<{ email: string }, { phone: string }>
// 必须传 email 或 phone,不能两个都传
高级:条件类型 + infer
// 提取数组元素类型
type ArrayElement<T> = T extends (infer E)[] ? E : never
type Item = ArrayElement<string[]> // string
// 提取函数返回类型(重新实现 ReturnType)
type MyReturnType<F> = F extends (...args: any[]) => infer R ? R : never
// 提取 Promise 解析后类型(重新实现 Awaited)
type Unpromise<T> = T extends Promise<infer V> ? V : T
infer 让你在条件类型里"声明并捕获"一个类型变量。
字符串字面量类型
type CSSVar<S extends string> = `--${S}`
type X = CSSVar<'primary'> // "--primary"
type ToCamelCase<S extends string> = S extends `${infer A}-${infer B}${infer Rest}`
? `${A}${Uppercase<B>}${ToCamelCase<Rest>}`
: S
type C = ToCamelCase<'foo-bar-baz'> // "fooBarBaz"
模板字面量类型 + infer 让 TS 能做一些"字符串编程"。但别滥用——
复杂的会让编辑器卡 + 错误信息难懂。
类型守卫
function isError(x: unknown): x is Error {
return x instanceof Error
}
try { ... } catch (e) {
if (isError(e)) {
console.error(e.message) // TS 知道 e 是 Error
}
}
x is Type 让 TS 在 if 分支里 narrow 类型。
type vs interface 何时选
- 简单对象 / 可被扩展(库提供的):interface
- 联合 / 交叉 / 字面量 / 复杂条件:type
- 不知道选哪个:type(更通用)
性能上几乎无差。
工具:type-fest
type-fest 收集了 100+ 实用
utility type:
npm i type-fest
import type { SetRequired, JsonValue, CamelCase } from 'type-fest'
type User = { name?: string; age?: number }
type WithName = SetRequired<User, 'name'>
不要每个项目都自己实现 DeepPartial,引这个库直接用。
踩过的坑
Partial<DeepFoo>不递归。要DeepPartial<...>。Exclude<T, U>看似"减去",但 U 必须是 T 的子集;不是的话什么都不去。as Type强制断言不安全(编译器信你的)。能用类型守卫就用。- 复杂 conditional type 报错时 TS 错误信息可达几十行。把复杂 type 拆
成中间命名 type,错误位置更清楚。
登录后参与评论。