你的代码库,其实一直在跟你交流。它不是通过报错,也不是通过警告,而是用一种更隐晦的方式:阻力(friction)。
你会在这些时刻清晰地感知到它的存在:一次小改动,却波及了六个文件;一次看似简单的重构,却让你深陷逻辑迷宫;或者,当你翻出几个月前写的代码时,突然意识到“这一半逻辑根本就不该存在”。
很多时候,问题并非源于设计失误,而仅仅是因为你没有善用 TypeScript 已经提供给你的工具。下面这 10 个特性并不炫技,也不复杂,但一旦正确使用,你的代码将变得更清晰、更稳定、更不容易在日后反噬你自己。
1️⃣ satisfies —— 更安全的对象校验方式
多数开发者习惯于使用 as 进行类型断言。但这存在一个根本性问题:as 会说谎。它强行告诉 TypeScript:“相信我,这个对象就是这个类型。” 而 satisfies 恰恰相反,它会:
- 校验对象结构是否符合预期类型。
- 不锁死对象的推断类型,保留额外的灵活性。
type Config = {
retries: number
mode: "safe" | "fast"
}
const cfg = {
retries: 3,
mode: "fast",
debug: true
} satisfies Config
在此例中,debug 属性可以存在,但它不会被视作 Config 类型的一部分。
为什么重要?
- 配置对象
- API 负载(Payload)
- 依赖映射
👉 在保证结构正确性的同时,不限制额外的灵活性。
权衡
如果你必须禁止对象出现任何多余字段,那么应该使用“精确对象类型”校验,而非 satisfies。
2️⃣ 用 in 操作符做类型收窄,让分支更清晰
处理联合类型(Union Types)时,许多人会堆砌大量的 if/else 语句。而 in 操作符提供了一种更直接、更精确的类型收窄方式。
type S =
| { kind: "ok"; value: number }
| { kind: "err"; message: string }
function handle(r: S) {
if ("value" in r) {
return r.value * 2
}
return `Error: ${r.message}`
}
为什么好用?
- 分支逻辑一目了然。
- 代码审查(Code Review)速度更快。
- 减少了冗余或错误的逻辑判断。
实际影响
在一个真实项目(包含约 2000 处联合类型判断)中,改用 in 操作符进行判断后,代码体积减少了约 6%。
3️⃣ as const —— 字面量冻结与精准类型推断
很多开发者选择使用联合类型替代 enum,但往往忘记了“冻结”字面量值。
const COLORS = ["red", "green", "blue"] as const
type Color = typeof COLORS[number]
// 现在 Color 类型是:“red” | “green” | “blue”
为什么重要?
- 防止运行时被意外修改:
COLORS 数组本身及其元素都变为只读。
- 自动补全精准到位:基于精确的字面量类型,编辑器能提供最准确的代码提示。
- 零重复定义:类型定义直接源自常量值,杜绝了不一致。
4️⃣ 模板字面量类型 —— 让字符串也拥有结构
许多与字符串相关的 Bug,本质上都是格式错误:拼写错误、缺少前缀、顺序错乱等。模板字面量类型正是为此而生。
type LogLevel = "info" | "warn" | "error"
type Tag = "auth" | "cache"
type LogKey = `${LogLevel}.${Tag}`
const key: LogKey = "warn.cache" // 正确
// const key2: LogKey = "debug.auth"; // 错误:类型“"debug"”不能赋值给类型“LogLevel”
为什么重要?
- 无需运行时解析:格式校验在编译期完成。
- 编译期保证格式正确:从根本上杜绝了拼写和格式错误。
注意点
如果组合的规模极其巨大(例如超过 10 万种),类型检查性能可能会下降,需谨慎使用。
5️⃣ readonly —— 成本最低的防御性编程
许多数组和对象自始至终都不需要被修改,但 TypeScript 的默认设定是所有内容都是可变的。
function safeAverage(nums: readonly number[]) {
return nums.reduce((a, b) => a + b, 0) / nums.length
}
为什么重要?
- 防止工具函数内部“顺手”修改数据:从接口层面杜绝了副作用。
- 意图表达清晰:函数签名明确告知调用者:“我只读取数据,绝不修改它”。
实际效果
在一个 30 万行代码 的项目中,将大约 40% 的工具函数参数改为 readonly 后,直接消灭了两类因意外数据修改(mutation)引发的 Bug。
6️⃣ never —— 用来“查漏补缺”的守卫类型
never 类型不是让你主动去写的,而是让你用来做完整性检查的。
function exhaust(x: never) {}
function process(s: S) {
switch (s.kind) {
case "ok": return s.value
case "err": return s.message
default: return exhaust(s) // 如果 S 新增了分支,这里会报错
}
}
一旦联合类型 S 新增了分支(例如增加 { kind: “timeout”; duration: number }),exhaust(s) 处的参数 s 将无法被赋值为 never,TypeScript 会立刻报错,提示你处理新增的情况。
为什么重要?
- 防止新增功能时遗漏逻辑处理。
- 强制实现“完整处理”,提升代码的健壮性。
👉 never 是联合类型的防漏网。
7️⃣ 映射类型 —— 彻底消灭重复的类型定义
在处理 DTO(数据传输对象)、API 响应结构、更新结构时,我们经常发现它们之间只是规则(如可选性、只读性)略有不同。映射类型能完美解决这类问题。
type Model = {
id: string
name: string
active: boolean
}
type PartialModel = {
[K in keyof Model]?: Model[K]
}
// 等价于 { id?: string; name?: string; active?: boolean; }
为什么重要?
- 无需复制粘贴字段定义。
- 重构时安全无忧:修改基础类型
Model,所有衍生类型(如 PartialModel)会自动同步更新。
什么时候不该用?
如果各个字段在未来很可能“各走各路”,拥有独立的演化路径,那么不要过度抽象,分别定义独立的类型可能是更好的选择。
8️⃣ 可辨识联合(Discriminated Unions)—— 实现确定性的控制流
可选字段(?)会引入“不确定性”(这个字段到底有没有?)。而可辨识联合则通过一个共有的、字面量类型的字段(通常名为 type 或 kind),创造出确定性。
type Event =
| { type: "created"; id: string }
| { type: “deleted”; id: string; reason: string }
function log(ev: Event) {
if (ev.type === "created") {
return `Created ${ev.id}`
}
// 在这个分支,TypeScript 知道 ev.type 一定是 “deleted”
return `Deleted ${ev.id} (${ev.reason})`
}
你得到的好处
- 不再需要编写
undefined 判断。
- 自动补全更加智能。
switch 或 if 语句能强制处理所有情况,配合 never 使用效果更佳。
9️⃣ 别只会用 Partial 和 Pick,善用其他工具类型
TypeScript 内置的工具类型远不止 Partial 和 Pick。
三个极具价值的工具类型:
Record<K, V> —— 明确表达键值映射关系,比 { [key: string]: V } 更精确。
ReturnType<T> —— 获取函数类型的返回类型,确保类型定义与函数实现严格对齐。
InstanceType<T> —— 获取构造函数的实例类型。
class Store {
fetch() {
return { ok: true }
}
}
type FetchResult = ReturnType<Store[“fetch”]>
// FetchResult 类型为 { ok: boolean }
为什么重要?
- 让类型定义跟随实现走,而不是相反。
- 大幅减少“类型漂移”(Type Drift),即类型定义与实际运行时结构逐渐脱节的问题。
实际效果
在一个包含 60 个接口的 API 客户端项目中,使用 ReturnType 替代手动编写的响应类型后,类型不一致的问题减少了约 15%。
🔟 使用 unknown,彻底抛弃 any
any 类型具有“传染性”。一次为了方便而使用 any,可能会导致整条调用链的类型安全荡然无存。unknown 则不同,它会强迫你在使用前进行类型检查。
function parse(json: string): unknown {
return JSON.parse(json)
}
const data = parse(someRawString)
if (typeof data === "object" && data && "id" in data) {
console.log(data.id) // 安全地访问
}
为什么重要?
- 划定清晰的信任边界。
- 对外部输入保持不信任。
- 在编译期逼迫开发者进行验证,将运行时错误提前暴露。
特别适合场景:
- 解析外部 API 的响应。
- 处理用户输入。
- 集成无类型声明的第三方库。
把这些特性组合起来,会怎样?
你将构建出一个清晰、健壮的模型处理管道:
Raw Input (unknown)
│
▼
Validator ---- satisfies ---- 内部类型
│
▼
Readonly Model ---- 映射 → Partial / Update
这并非“炫技的类型体操”,而是 将你的设计意图清晰地写进类型系统里。当你开始综合运用 satisfies、readonly、模板字面量类型、映射类型和可辨识联合时,你会显著感觉到:
- 重构的影响范围更小、更可控。
- 代码审查(Code Review)的速度更快。
- 错误会出现在它们“该出现的地方”——编码阶段,而不是运行时。
更干净的 TypeScript 代码,并非依赖于庞大而复杂的抽象,而是依赖于这些小巧、精准且目的明确的语言特性。掌握它们,你的代码才能真正做到“像它看起来一样可靠”。
如果你对 ES6+ 新特性如何与 TypeScript 结合使用有更多疑问,欢迎在技术社区交流探讨。