学径XUEJING · 个人学习平台通知设置

课程库 TypeScript TypeScript 起步 › 第 5

联合类型与收窄 —— "可能是 A 也可能是 B"怎么安全处理

普通课55 分钟

本课导读

真实世界的数据经常"不确定":用户 ID 可能是数字也可能是字符串、可选字段可能压根没给、订单状态是几个词中的一个。TS 对付不确定的武器是一对组合拳:联合类型(用 | 声明"可能是 A 也可能是 B")+ 收窄(用 if 检查把可能性一步步排除,编译器全程跟着你缩小范围)。这是 TS 最有味道的设计——你写普通的判断逻辑,类型自动变准。预计 55 分钟。

联合类型:竖线连起可能性

前两课 string | undefined 已经两次露脸,今天正式介绍。A | B 读作"A 或 B":

TypeScript

声明"可能是 A 或 B"很简单。真正的问题在使用——对一个"可能是两种东西"的值,你能干什么?

TypeScript

报错说得很讲理:toUpperCase 是字符串的方法——万一 id 是数字呢?联合类型的铁律:只能直接使用所有候选类型"共有"的成员(比如 toString 谁都有)。想用某一边独有的能力,得先证明"现在它就是那一边"——这就是收窄。

收窄:用 if 说服编译器

TypeScript

看清这里发生的魔法:

  • typeof id === "string" 是一句普通的运行时判断(typeof 是 JS 老功能)——但 TS 在编译期读懂了它:if 分支里 id 收窄为 string,else 侧自动排除剩 number
  • 你没写任何类型语法,类型跟着控制流走——这叫控制流分析,TS 的招牌本事
  • 上一课"可选字段用前判断 undefined",现在有了正式名分:if (x !== undefined) 就是对 string | undefined 的收窄

提示

类比记忆:联合类型像安检口的双通道包裹(可能是液体也可能是电子产品),收窄就是过一遍扫描仪——扫完确认是电子产品,才允许按电子产品的规矩处理。不扫就直接拆(不判断就调方法),安检员(编译器)当场拦下。

字面量联合:比 string 更严格的字符串

联合的候选不仅能是类型,还能是具体的值——这是 TS 一个极其高频的实战招式:

TypeScript

两个新知识一次到位:

  • 字面量联合 "pending" | "paid" | ...:这个参数不是"任意字符串",而是这四个词之一——状态、档位、模式这类"有限选项"场景的标配。拼成 "payed" 的惨案(纯 JS 里要线上才发现)从此绝迹
  • type 别名type 名字 = 类型 给任何类型起名复用。和 interface 的分工现阶段这么记:形状用 interface,联合/别名用 type,够用两年

收窄的第三板斧:in

typeof 认基础类型、!== undefined 排除空值,第三板斧对付"两种形状"——in 检查字段在不在:

TypeScript

收窄三板斧集齐:typeof(基础类型)、!== undefined(可选/空值)、in(形状之间)。它们的共同点:都是普通的运行时判断,TS 顺着你的逻辑自动把类型越缩越准——好的 TS 代码,就是把"你脑子里的分类讨论"写成 if,类型系统全程陪跑

✍️ 练习

练习 1:价格标签

写函数 priceTag(price: number | string):数字则格式化为 "¥599",字符串(如 "面议")则原样加括号返回 "(面议)"

TypeScript
✅ 参考答案
function priceTag(price: number | string): string {
    if (typeof price === "number") {
        return "¥" + price;
    }
    return "(" + price + ")";
}

console.log(priceTag(599));
console.log(priceTag("面议"));

typeof 收窄的最小完整用例:if 里是 number 的世界,if 外是 string 的世界,各用各的写法。

练习 2:会员等级机

定义字面量联合 type Level = "bronze" | "silver" | "gold",写函数 upgrade(level: Level): Level 返回下一级(gold 封顶返回自己)。然后故意用错误的等级词调用一次,观察报错。

TypeScript
✅ 参考答案
type Level = "bronze" | "silver" | "gold";

function upgrade(level: Level): Level {
    if (level === "bronze") {
        return "silver";
    }
    if (level === "silver") {
        return "gold";
    }
    return "gold";
}

console.log(upgrade("bronze"));
console.log(upgrade("gold"));

upgrade("diamond") 会报 TS2345——不存在的等级词进不了函数。返回类型标 Level 还有个隐藏福利:函数体里手滑 return "glod" 也会当场被抓。

练习 3:消息分诊台(挑战)

系统消息有两种形状:文本消息 { text: string }、转账消息 { amount: number; from: string }。写 handle(msg) 函数:文本返回 "[文本] ...",转账金额 ≥ 1000 返回 "[大额转账] 来自xxx",否则 "[转账] 来自xxx"。三板斧选对工具。

TypeScript
✅ 参考答案
interface TextMsg {
    text: string;
}
interface TransferMsg {
    amount: number;
    from: string;
}

function handle(msg: TextMsg | TransferMsg): string {
    if ("text" in msg) {
        return "[文本] " + msg.text;
    }
    if (msg.amount >= 1000) {
        return "[大额转账] 来自" + msg.from;
    }
    return "[转账] 来自" + msg.from;
}

console.log(handle({ text: "开会啦" }));
console.log(handle({ amount: 5000, from: "老王" }));
console.log(handle({ amount: 200, from: "小陈" }));

形状之间用 in 收窄;进入转账分支后 msg.amount、msg.from 随便点——TS 已确认它是 TransferMsg。这个"消息分诊"模式在真实前端代码里出场率极高(各种事件、通知、API 响应)。

📝 随堂测验

随堂测验0 / 5 题正确

1. number | string 类型的值,不做任何判断直接调 .toUpperCase() 会?

2. if (typeof id === "string") 分支内,id 的类型变成了?

3. type OrderStatus = "pending" | "paid" 这类字面量联合的最大价值是?

4. interface 和 type 的分工,本课程的够用版口诀是?

5. 收窄三板斧和各自的主场是?

本课小结

  • 联合类型 A | B:声明不确定;只能直接用共有成员,独家能力先收窄
  • 收窄三板斧:typeof / !== undefined / in——普通判断,类型跟着控制流自动变准
  • 字面量联合 + type 别名:把"有限选项"写进类型,错别字状态绝迹;口诀"形状 interface、联合 type"
  • 可选字段/参数的 | undefined,至此完全收网

下一课最后一块拼图:泛型——当一个函数想对"任何类型"通用、又不想丢掉类型保护时,<T> 登场。

划选正文任意文字可高亮、批注或加入复习卡

讨论

载入中…