前端进阶之旅前端进阶之旅
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
  • 基础篇HTML/CSS/JS 打底
  • 进阶篇原理与工程化
  • 高频篇面试最常问的那批
  • 精选篇按模块收敛的总结
  • 手写篇常考代码手写实现
  • 面经篇真实面试问题复盘
  • AI 篇NEWAI 时代的前端考点
  • 历年面经NEW按年份追踪真实考点
  • 每日一题每天一道,攒手感
  • 专项自测100 题快速查漏
  • 小程序题库小程序专项刷题
  • 算法题库NEW在线编码即时判题
  • 知识卡片NEW碎片时间过考点
  • 面试题大全常见问题解析
  • AI 答疑NEW随时提问,即时解析
  • AI 模拟面试NEW模拟真实面试 + 报告
  • AI 定制路线NEW按你的简历现排
  • AI 知识地图NEW串起全站知识点
  • 原理篇React / Vue 源码拆解
  • HTTP从报文一路讲到 HTTPS
  • 浏览器渲染、事件循环、进程
  • 计算机基础Linux、网络、操作系统
  • 设计模式23 种模式怎么用
  • Node学习指南从环境搭建到服务端
  • NPM工作流script、依赖与发布
  • Docker容器化部署上手
  • Canvas图形与动画实战
  • 前端系统进阶学习大型项目工程化
  • 前端综合文章长期沉淀的实践文
  • 思维导图知识点全景图
  • 学习路线按图索骥不跑偏
  • AI 热点NEWAI 每日动态
  • 公众号动态公众号历史文章
  • 博客动态站长的技术博客
  • 开发者导航常用工具与文档站
首页程序员面试题库TypeScript 判别联合 discriminated union 建模
TyTypeScript类型收窄

TypeScript 判别联合如何建模,为什么标签写成 string 后不能正确收窄?

真正需要保留的是标签值与整组字段之间的关联,而不是只给一个大对象增加状态属性。

前端进阶之旅 · 一题精讲更新于 2026.09.06
TypeScript#类型收窄#类型系统
先看核心答案读代码示例
理解线索

把一组状态作为整体描述

  1. 共同标签所有候选都能读取的状态识别字段
  2. 成员负载只在对应状态成立时必需的数据
  3. 分支关联检查标签后同时获得该成员的字段能力

标签可使用布尔或数字等可区分取值,重叠标签只能缩小到仍共享该值的候选集合。

核心回答

先记住这个答案

判别联合把不同状态分别定义为对象成员,并用公共字段中的可区分取值建立标签与成员的关联。常见写法是 kind 为不同字符串字面量,分支各自拥有必需数据;检查 kind 后,TypeScript 就能选出对应成员。若每个标签都写成宽 string,某个具体字符串无法排除其他成员,专有字段便不能按预期使用。把所有数据放进同一对象并设成可选,也没有表达状态与字段同时成立的关系。设计时应让真正互斥的状态在类型中保持互斥。

  • 每个成员绑定自己的标签与必需字段
  • 宽 string 标签无法提供唯一的分支关联
  • 可选字段大对象会允许无意义的状态组合

为什么要拆成多个对象成员

下载完成必然有文件地址,下载失败必然有原因。如果只写一个状态字段,再把地址与原因都声明成可选,类型会允许完成却没有地址的对象。使用者即使检查状态,也无法从这份模型中证明可选字段已经存在。

示例把两种状态拆开,再用公共 kind 关联数据。分支里可以直接使用必需字段,构造缺少地址的完成状态会被拒绝。这样类型约束同时服务生产者和消费者,避免只有读取端反复断言字段存在,而创建端仍能构造错误组合。

让状态与负载共同组成联合成员TypeScript
type Download =
  | { kind: 'done'; url: string }
  | { kind: 'failed'; reason: string };
function summary(value: Download): string {
  switch (value.kind) {
    case 'done': return value.url;
    case 'failed': return value.reason;
  }
}
const finished: Download = { kind: 'done', url: '/report.pdf' };
// @ts-expect-error 完成状态必须提供 url
const broken: Download = { kind: 'done' };

标签判断选中整个成员,因此 done 分支拥有必需 url,failed 分支拥有必需 reason。缺少地址的完成对象在创建时就报错,不能靠标签看起来正确而绕过负载契约。

标签拓宽为什么会丢失关联

如果两个成员的 kind 都是 string,它们都可能在运行时等于 done,检查这个值就不能证明当前是哪一支。常见诱因是先创建可变对象,标签属性被推断成 string,随后再尝试传入有限联合。应在构造边界提供适当注解或保留字面量。

即使使用字面量,如果多个成员共享同一个标签值,检查后仍可能剩下多个候选,专有字段也未必全部可用。标签不必拘泥于字符串,但必须能为所需区分提供足够证据。不要把某个固定三条件口诀当作所有高级联合行为的完整规范。

从接口数据到状态模型的边界

外部响应中的标签和字段仍需要验证,服务端返回 done 却没有地址时,静态声明不会替你补齐。可以在解析层把协议映射为受控内部联合,并为未知状态设计明确失败或兼容路径;进入内部逻辑后再依赖分支收窄。

状态增加时,还应检查所有消费者是否补齐处理。显式返回契约可以发现部分遗漏,never 穷尽检查则能直接指出尚未处理的成员。对于嵌套流程,应在每个状态层级保存对应负载,不要让一个标签承担所有彼此独立状态的组合。

回答前,多想一步

容易答错的地方

只要有 kind 字段就是判别联合
如果标签是无法区分成员的宽类型,或所有负载都散落在可选字段中,检查标签并不能自动建立完整的状态与数据关联。
每个标签字面量必须只对应一个字段
标签对应的是整个成员结构,可以携带多项必需数据;若同一标签被多个成员共享,收窄后可能仍保留那些成员的联合。
试着用自己的话回答

面试官还会怎么问?

标签能使用 true 和 false 吗?

可以用布尔字面量区分两个成员,但必须把对应数据分别绑定到两支;仅有 boolean 和两个可选字段的大对象仍未表达关联。

为什么先存变量再传入时标签会报错?

对象属性可能因为可变性推断成 string,超出目标联合允许的字面量集合;应在创建边界用明确类型或合适的常量推断保留状态信息。

可以用标签完全替代运行时校验吗?

不能,外部对象可能带正确标签却缺少对应字段;解析层需要验证成员完整性,之后内部状态处理才能可靠依赖这个联合契约。

从一道题,走向一组知识

把知识连起来

类型系统基础

TypeScript 如何用 never 检查 switch 是否处理了全部联合分支?

让新增联合状态提醒所有尚未更新的处理点

类型系统基础

TypeScript 为什么把对象中的字符串推断成 string,如何保留字面量?

在对象创建位置保留能够区分分支的字面量

类型收窄

TypeScript 嵌套判别联合怎么收窄,检查内层标签为什么不一定选中外层对象?

复杂流程需要按层级保持状态与负载关联

参考资料

  • TypeScript:Discriminated Unions

示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。

本题目录
  1. 先记住这个答案
  2. 为什么要拆成多个对象成员
  3. 标签拓宽为什么会丢失关联
  4. 从接口数据到状态模型的边界
  5. 容易答错的地方
  6. 面试官还会怎么问
  7. 把知识连起来
读懂,再试着讲出来

先看核心答案,再读代码。最后展开追问,检查自己有没有遗漏边界。

试着回答追问
浏览全部面试题理解原理,也关注真实的使用场景。回到顶部 ↑