精确的类型控制:联合类型、类型守卫与断言
需求场景
订单系统的支付模块需要处理多种支付结果:银行卡支付返回交易流水号(字符串),余额支付返回扣减后的余额(数字),第三方支付返回一个包含跳转 URL 的对象。同一个 processPaymentResult 函数需要根据支付方式的不同,对不同类型的返回值做出相应处理。
这种「一个变量可能是多种类型」的场景,正是 TypeScript 高级类型系统的用武之地。本章深入讲解如何用联合类型描述多态数据、用类型守卫实现精确的分支判断、用类型断言处理编译器无法自动推断的情况。
联合类型
基本用法
联合类型用 | 分隔多个类型,表示变量可以是其中任何一种:
let paymentRef: string | number;
paymentRef = "TXN-20260312-001"; // 合法
paymentRef = 10042; // 合法
paymentRef = true; // 编译错误
核心规则:编译器不确定具体类型时,只允许访问所有成员类型共有的属性和方法。
function displayRef(ref: string | number): string {
return ref.toString(); // 通过:string 和 number 都有 toString
}
function getRefLength(ref: string | number): number {
return ref.length; // 编译错误:number 没有 length
}
赋值后的自动收窄
变量被赋予具体值后,编译器自动推断当前类型:
let result: string | number;
result = "success";
console.log(result.toUpperCase()); // 此处推断为 string,通过
result = 404;
console.log(result.toFixed(0)); // 此处推断为 number,通过
字面量联合类型
用具体的值构成联合类型,限定变量的合法取值范围:
type PayChannel = "alipay" | "wechat" | "card" | "balance";
function initiatePayment(channel: PayChannel, amount: number): void {
console.log(`Paying ${amount} via ${channel}`);
}
initiatePayment("alipay", 99); // 通过
initiatePayment("paypal", 99); // 编译错误:不在允许的值中
这比使用宽泛的 string 类型更安全,编译器能在编码阶段拦截拼写错误。
交叉类型
交叉类型用 & 连接,表示变量必须同时满足所有类型的要求:
interface HasTraceId {
traceId: string;
}
interface HasTimestamp {
createdAt: number;
}
interface HasOperator {
operatorId: string;
}
type AuditRecord = HasTraceId & HasTimestamp & HasOperator;
const record: AuditRecord = {
traceId: "T-001",
createdAt: Date.now(),
operatorId: "admin-01",
};
交叉类型常用于组合多个独立关注点。如果交叉的类型中存在同名但类型冲突的属性,该属性变为 never:
interface X { tag: string }
interface Y { tag: number }
type Z = X & Y;
// Z 的 tag 类型为 string & number,即 never
类型收窄
类型收窄(Type Narrowing)是 TypeScript 的核心能力之一。面对联合类型时,需要通过各种守卫手段将类型缩小到具体分支。
typeof 守卫
最常用的守卫,适用于基础类型判断:
function formatPaymentRef(ref: string | number): string {
if (typeof ref === "string") {
return ref.toUpperCase();
}
return `#${ref.toString().padStart(8, "0")}`;
}
typeof 能识别的类型有限:"string"、"number"、"boolean"、"symbol"、"bigint"、"undefined"、"object"、"function"。
instanceof 守卫
用于判断值是否为某个类的实例:
class PaymentTimeout extends Error {
retryAfter: number;
constructor(msg: string, retry: number) {
super(msg);
this.retryAfter = retry;
}
}
class PaymentDeclined extends Error {
declineCode: string;
constructor(msg: string, code: string) {
super(msg);
this.declineCode = code;
}
}
function handlePaymentError(err: PaymentTimeout | PaymentDeclined): string {
if (err instanceof PaymentTimeout) {
return `Timeout. Retry in ${err.retryAfter}s`;
}
return `Declined: ${err.declineCode}`;
}
in 运算符守卫
检查对象是否包含某个属性,特别适合区分接口类型(接口在运行时不存在,无法用 instanceof):
interface CardPayment {
cardLast4: string;
processCharge(): void;
}
interface BalancePayment {
currentBalance: number;
deductBalance(amount: number): void;
}
function executePayment(method: CardPayment | BalancePayment): void {
if ("cardLast4" in method) {
console.log(`Charging card ending in ${method.cardLast4}`);
method.processCharge();
} else {
console.log(`Balance: ${method.currentBalance}`);
method.deductBalance(100);
}
}
自定义类型守卫(is 关键字)
当内置守卫无法满足需求时,可以编写返回类型为 参数 is 类型 的守卫函数:
interface SuccessResult {
status: "success";
data: string;
}
interface FailureResult {
status: "failure";
errorMsg: string;
}
function isSuccess(result: SuccessResult | FailureResult): result is SuccessResult {
return result.status === "success";
}
function handleResult(result: SuccessResult | FailureResult): void {
if (isSuccess(result)) {
console.log(`Data: ${result.data}`); // 此处编译器知道是 SuccessResult
} else {
console.log(`Error: ${result.errorMsg}`); // 此处编译器知道是 FailureResult
}
}
is 关键字的核心价值:将运行时的判断逻辑反馈给编译器,让后续代码自动获得类型收窄。
可辨识联合
可辨识联合(Discriminated Unions)是一种强大的设计模式:为联合类型的每个成员添加一个共有的字面量属性作为「判别标签」。
interface OrderCreatedEvent {
type: "order_created";
orderId: string;
amount: number;
}
interface OrderPaidEvent {
type: "order_paid";
orderId: string;
transactionId: string;
}
interface OrderShippedEvent {
type: "order_shipped";
orderId: string;
trackingNo: string;
carrier: string;
}
type OrderEvent = OrderCreatedEvent | OrderPaidEvent | OrderShippedEvent;
function processEvent(event: OrderEvent): void {
switch (event.type) {
case "order_created":
console.log(`Order ${event.orderId} created, amount: ${event.amount}`);
break;
case "order_paid":
console.log(`Order ${event.orderId} paid, txn: ${event.transactionId}`);
break;
case "order_shipped":
console.log(`Order ${event.orderId} shipped via ${event.carrier}, tracking: ${event.trackingNo}`);
break;
}
}
可辨识联合的三个要素:
- 共有的字面量属性(
type) - 由多个带判别属性的类型组成联合类型
- 基于判别属性的 switch 或 if 分支
穷尽性检查
用 never 确保 switch 覆盖了所有分支:
function processEvent(event: OrderEvent): string {
switch (event.type) {
case "order_created":
return `Created: ${event.orderId}`;
case "order_paid":
return `Paid: ${event.orderId}`;
case "order_shipped":
return `Shipped: ${event.orderId}`;
default:
const _guard: never = event;
return _guard;
}
}
如果将来新增了 OrderCancelledEvent 但忘记添加对应的 case,编译器会在 default 分支报错。
类型断言
as 语法
类型断言让开发者手动指定值的类型,告诉编译器「我确定这个值是什么类型」:
// 推荐语法
const elem = document.getElementById("root") as HTMLDivElement;
// 旧语法(.tsx 文件中不可用)
const elem2 = <HTMLDivElement>document.getElementById("root");
断言的约束
断言不是随意的类型转换。TypeScript 要求被断言的类型和目标类型之间存在兼容关系:
const val = 42;
const str: string = val as string; // 编译错误:number 和 string 不兼容
断言不是类型转换
断言纯粹是编译时概念,不会改变值本身:
function asBoolean(val: any): boolean {
return val as boolean;
}
asBoolean(1); // 返回 1,不是 true
需要真正的类型转换,使用对应的构造函数:Boolean(1) 返回 true。
非空断言
! 操作符告诉编译器某个值一定不是 null 或 undefined:
function getOrderTotal(orderId?: string): number {
// 确信 orderId 在此处一定有值
return parseInt(orderId!.split("-")[1]);
}
在 DOM 操作中常见:
const container = document.querySelector("#app")!;
container.innerHTML = "Loading...";
使用建议:非空断言绕过了编译器的空值检查。优先考虑用可选链 ?. 或条件判断处理可能的空值。
双重断言
当两个类型之间不存在直接兼容关系时,可以通过 unknown 做中间桥梁:
interface InputConfig {
validate(): boolean;
}
interface OutputConfig {
serialize(): string;
}
const input: InputConfig = { validate: () => true };
const output = input as unknown as OutputConfig;
这种做法绕过了类型系统的保护,仅在确实无法通过其他方式表达类型关系时使用(如测试代码中构造 mock 对象、与无类型的旧 JavaScript 库交互)。
断言函数
TypeScript 3.7 引入了断言函数,通过 asserts 关键字声明:如果函数正常返回则参数满足指定类型,否则抛出异常。
function assertDefined<T>(val: T | null | undefined, msg: string): asserts val is T {
if (val === null || val === undefined) {
throw new Error(msg);
}
}
function processOrder(orderId: string | null): void {
assertDefined(orderId, "Order ID is required");
// 此处 orderId 被收窄为 string
console.log(orderId.toUpperCase());
}
断言函数 vs 类型守卫函数:
- 类型守卫:返回
boolean,用于 if 分支 - 断言函数:返回
void,不满足条件时抛异常
// 类型守卫
function isString(val: unknown): val is string {
return typeof val === "string";
}
// 断言函数
function assertString(val: unknown): asserts val is string {
if (typeof val !== "string") throw new TypeError("Expected string");
}
as const 断言
as const 将值的类型收窄为最精确的字面量类型,并使其变为只读:
// 普通声明
const routes1 = ["/home", "/orders", "/profile"];
// 类型:string[]
// as const 断言
const routes2 = ["/home", "/orders", "/profile"] as const;
// 类型:readonly ["/home", "/orders", "/profile"]
// 对象使用 as const
const endpoints = {
orders: "/api/orders",
products: "/api/products",
users: "/api/users",
} as const;
// 所有属性变为 readonly,值类型为字面量
as const 在需要把数组作为元组参数传递时特别有用:
function createRoute(method: string, path: string): void {}
const routeDef = ["GET", "/api/orders"] as const;
createRoute(...routeDef); // 合法
综合实战:支付结果处理器
interface CardPayResult {
type: "card";
transactionRef: string;
last4: string;
}
interface BalancePayResult {
type: "balance";
remainingBalance: number;
}
interface ThirdPartyPayResult {
type: "redirect";
redirectUrl: string;
callbackToken: string;
}
type PaymentResult = CardPayResult | BalancePayResult | ThirdPartyPayResult;
function isRedirectResult(result: PaymentResult): result is ThirdPartyPayResult {
return result.type === "redirect";
}
function assertValidResult(result: unknown): asserts result is PaymentResult {
if (
typeof result !== "object" ||
result === null ||
!("type" in result)
) {
throw new Error("Invalid payment result");
}
}
function handlePaymentResult(rawResult: unknown): string {
assertValidResult(rawResult);
if (isRedirectResult(rawResult)) {
return `Redirect to: ${rawResult.redirectUrl}`;
}
switch (rawResult.type) {
case "card":
return `Card charged (****${rawResult.last4}), ref: ${rawResult.transactionRef}`;
case "balance":
return `Balance deducted, remaining: ${rawResult.remainingBalance}`;
default:
const _exhaustive: never = rawResult;
return _exhaustive;
}
}
本章回顾
| 概念 | 语法 | 用途 | 注意事项 |
|---|---|---|---|
| 联合类型 | A | B | 值可以是多种类型之一 | 只能访问共有属性 |
| 交叉类型 | A & B | 值同时满足多种类型 | 属性冲突变为 never |
| typeof 守卫 | typeof x === "string" | 收窄基础类型 | 只识别有限种类型 |
| instanceof 守卫 | x instanceof Class | 收窄类实例 | 不适用于接口 |
| in 守卫 | "prop" in obj | 属性存在性判断 | 适合接口类型区分 |
| 自定义守卫 | x is Type | 封装复杂判断逻辑 | 返回 boolean |
| 可辨识联合 | type: "literal" | 精确匹配联合分支 | 需共有字面量标签 |
| 类型断言 | x as Type | 手动指定类型 | 仅编译时,不做运行时转换 |
| 非空断言 | x! | 声明值非 null/undefined | 谨慎使用 |
| 双重断言 | x as unknown as Type | 强制不兼容的类型转换 | 极度危险 |
| 断言函数 | asserts x is Type | 通过抛异常保证类型 | 返回 void |
| as const | value as const | 收窄为字面量只读类型 | 只适用于字面量值 |
购买课程解锁全部内容
告别类型错误:12 章掌握 TypeScript 工程实战
¥29.90