跳到主要内容
预计阅读 26 分钟

精确的类型控制:联合类型、类型守卫与断言

需求场景

订单系统的支付模块需要处理多种支付结果:银行卡支付返回交易流水号(字符串),余额支付返回扣减后的余额(数字),第三方支付返回一个包含跳转 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;
  }
}

可辨识联合的三个要素:

  1. 共有的字面量属性(type
  2. 由多个带判别属性的类型组成联合类型
  3. 基于判别属性的 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

非空断言

! 操作符告诉编译器某个值一定不是 nullundefined

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 constvalue as const收窄为字面量只读类型只适用于字面量值

购买课程解锁全部内容

告别类型错误:12 章掌握 TypeScript 工程实战

¥29.90