联合、交叉、控制流收窄与穷尽检查
联合类型表达“多个可能之一”。可靠代码不靠断言选择分支,而是通过运行时证据让控制流分析逐步缩窄。
1. 常见收窄手段
function describe(value: string | number | Date): string {
if (typeof value === "string") return value.toUpperCase()
if (value instanceof Date) return value.toISOString()
return value.toFixed(2)
}
TypeScript 理解 typeof、instanceof、相等比较、真值判断、in、赋值、提前返回和部分标准库谓词。
typeof null 陷阱
JavaScript 中 typeof null === "object",因此对象检查必须排除 null:
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null
}
2. 用户定义类型谓词
type Fish = { swim(): void }
type Bird = { fly(): void }
function isFish(animal: Fish | Bird): animal is Fish {
return "swim" in animal
}
function move(animal: Fish | Bird): void {
if (isFish(animal)) animal.swim()
else animal.fly()
}
谓词签名是对实现正确性的承诺;若实现返回错误结果,检查器会被误导。复杂数据应使用经过测试的 schema 解析器,而非只检查一个属性。
3. 断言函数
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new TypeError("Expected string")
}
}
function normalize(value: unknown): string {
assertString(value)
return value.trim()
}
断言函数失败时必须中断正常控制流;若仅记录日志却继续返回,会破坏类型承诺。
4. 可辨识联合:状态机首选模型
type RequestState<T> =
| { status: "idle" }
| { status: "loading"; startedAt: number }
| { status: "success"; data: T }
| { status: "failure"; error: Error }
function render<T>(state: RequestState<T>): string {
switch (state.status) {
case "idle":
return "尚未开始"
case "loading":
return `加载中:${state.startedAt}`
case "success":
return JSON.stringify(state.data)
case "failure":
return state.error.message
default:
return assertNever(state)
}
}
function assertNever(value: never): never {
throw new Error(`Unhandled state: ${JSON.stringify(value)}`)
}
新增状态而未更新 switch 时,state 不再是 never,编译器会在默认分支报告错误。
5. 交叉类型与分布
type WithId = { id: string }
type WithVersion = { version: number }
type Entity = WithId & WithVersion
联合是集合并集,交叉是同时满足约束。条件类型遇到裸类型参数时会对联合分布,后续高级类型章节会详细说明。
6. 收窄会在哪些地方失效
type Box = { value?: string }
function print(box: Box): void {
if (box.value !== undefined) {
const snapshot = box.value
queueMicrotask(() => {
console.log(snapshot.toUpperCase())
})
}
}
可变对象属性可能在回调执行前改变。保存已收窄的不可变局部值,通常比依赖跨闭包属性收窄更稳定。
7. in 与可选属性
若某联合成员的属性是可选的,它可能同时出现在 in 判断的两个分支中;不要假设 "swim" in value 的 false 分支一定排除了含可选 swim 的类型。
8. 边界解析模式
type Message =
| { type: "text"; body: string }
| { type: "count"; value: number }
function parseMessage(input: unknown): Message {
if (!isRecord(input) || typeof input.type !== "string") {
throw new TypeError("Invalid message")
}
if (input.type === "text" && typeof input.body === "string") {
return { type: "text", body: input.body }
}
if (input.type === "count" && typeof input.value === "number") {
return { type: "count", value: input.value }
}
throw new TypeError("Unsupported message")
}
解析器返回新的、已验证对象,比把原输入整体断言成目标类型更清晰。