CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
TypeScript 核心/课时/联合与交叉类型
RawBlame
课时目录12 节

TypeScript 核心

  1. 01TypeScript 是什么
  2. 02基本类型
  3. 03函数类型
  4. 04接口与类型别名
  5. 05联合与交叉类型
  6. 06类型收窄
  7. 07泛型
  8. 08工具类型
  9. 09模块与 tsconfig
  10. 10React 组件 typing
  11. 11异步与 API 类型
  12. 12综合练习与 Next.js

联合与交叉类型

union、intersection、字面量类型。

30 分钟

联合与交叉类型

这一课你将能做到

  1. 用联合类型 | 表达「多选一」,用交叉类型 & 表达「全都要」
  2. 用字面量联合替代魔法字符串,写出精确的状态字段
  3. 掌握判别联合(Discriminated Unions)建模「不同形态的数据」,这是业务开发中最实用的类型技巧

为什么要学

真实世界的数据很少是单一形状的:一个接口可能返回成功或失败两种结构;一个字段可能是 string 也可能是 number;一个组件状态可能是「加载中 / 成功 / 失败」三选一。

联合类型 | 让你精确表达「这个值是这几种类型之一」;交叉类型 & 让你把多个类型合并成一个。而判别联合是 TS 建模的灵魂——后端返回的复杂 JSON、前端的多状态 UI,用判别联合建模后,编译器能帮你在每个分支里精确推导出可用字段,写错字段名当场报错。

先建立图像

两个运算符的方向感:

联合 A | B:  「或者」——值是 A 或 B 之一(集合的并集)
              ┌─────┐   ┌─────┐
              │  A  │ ∪ │  B  │   取值范围变【宽】
              └─────┘   └─────┘   可用能力变【窄】(只有共有成员)

交叉 A & B:  「并且」——值同时满足 A 和 B(形状的叠加)
              ┌─────────────┐
              │   A 的字段   │
              │      +      │   取值范围变【窄】(要同时满足)
              │   B 的字段   │   可用能力变【宽】(字段全都有)
              └─────────────┘

记忆口诀:联合取值变宽、能力变窄;交叉取值变窄、能力变宽。

跟着做

新建 unions-demo.ts,逐步实操。

步骤 1:联合类型基础

// id 可能是数字或字符串(后端常见情况)
function printId(id: number | string): void {
  console.log(`ID: ${id}`);
}

printId(101);     // OK
printId("abc");   // OK
// printId(true); // 报错

// 联合类型的使用限制:只能调用所有成员共有的方法
function getLength(value: string | string[]): number {
  // value.toUpperCase(); // 报错:string[] 没有这个方法
  return value.length;     // OK:string 和 string[] 都有 length
}

console.log(getLength("hello"));      // 5
console.log(getLength(["a", "b"]));   // 2

想使用某个成员特有的方法?先做类型收窄(下一课详讲):

function format(value: string | number): string {
  if (typeof value === "string") {
    return value.toUpperCase(); // 这个分支里 TS 知道 value 是 string
  }
  return value.toFixed(2);      // 这里 value 是 number
}

console.log(format("hello"), format(3.14159)); // HELLO 3.14

步骤 2:字面量联合——替代魔法字符串

// 不要这样:status 是任意 string,拼错都不报错
// let status: string = "pensding"; // 笔误没人管

// 要这样:取值限定为三个字面量之一
type OrderStatus = "pending" | "paid" | "shipped" | "cancelled";

let status: OrderStatus = "pending";
status = "paid";
// status = "pensding"; // 报错:笔误当场被抓

function statusText(s: OrderStatus): string {
  switch (s) {
    case "pending":   return "待支付";
    case "paid":      return "已支付";
    case "shipped":   return "已发货";
    case "cancelled": return "已取消";
  }
}

console.log(statusText(status)); // 已支付

字面量联合比枚举更轻量,是现代 TS 项目表达「固定取值集合」的首选。

步骤 3:交叉类型 &

interface Timestamped {
  createdAt: string;
  updatedAt: string;
}

interface Named {
  name: string;
}

// 同时满足两个形状
type NamedEntity = Timestamped & Named;

const item: NamedEntity = {
  name: "测试条目",
  createdAt: "2026-01-01",
  updatedAt: "2026-10-06",
};

console.log(item.name, item.createdAt); // 测试条目 2026-01-01

注意交叉原始类型会得到 never(不可能同时是 string 又是 number):

type Impossible = string & number; // never,没意义

交叉类型的正确用法是合并对象形状,不是交叉原始值。

步骤 4:判别联合(重点中的重点)

核心技巧:给每个形态加一个共同的、取值互斥的字面量字段(判别字段),TS 就能根据它自动收窄。

// 三种形状,都用 kind 字段做判别
interface LoadingState {
  kind: "loading";
}

interface SuccessState {
  kind: "success";
  data: string[];
}

interface ErrorState {
  kind: "error";
  message: string;
}

// 合成判别联合
type FetchState = LoadingState | SuccessState | ErrorState;

function render(state: FetchState): string {
  switch (state.kind) {
    case "loading":
      return "加载中...";
    case "success":
      // 这个分支里 TS 精确知道 data 存在
      return `拿到 ${state.data.length} 条数据`;
    case "error":
      // 这里 message 存在,但访问 state.data 会报错
      return `出错了:${state.message}`;
  }
}

console.log(render({ kind: "loading" }));                        // 加载中...
console.log(render({ kind: "success", data: ["a", "b"] }));      // 拿到 2 条数据
console.log(render({ kind: "error", message: "网络超时" }));      // 出错了:网络超时

感受这个威力:state.kind === "success" 的分支里,state.data 必然存在,编译器保证;你敢访问 state.message 直接红线。状态机的每个分支能用什么字段,类型系统说了算。

步骤 5:判别联合实战——API 响应建模

// 后端接口的典型两种返回
interface ApiSuccess {
  code: 0;
  data: { username: string; balance: number };
}

interface ApiFailure {
  code: 400 | 401 | 500;
  error: string;
}

type ApiResponse = ApiSuccess | ApiFailure;

function handleResponse(res: ApiResponse): void {
  if (res.code === 0) {
    // TS 知道这里是 ApiSuccess
    console.log(`欢迎 ${res.data.username},余额 ¥${res.data.balance}`);
  } else {
    // TS 知道这里是 ApiFailure
    console.log(`请求失败(${res.code}):${res.error}`);
  }
}

handleResponse({ code: 0, data: { username: "小明", balance: 99.5 } });
// 欢迎 小明,余额 ¥99.5
handleResponse({ code: 401, error: "登录已过期" });
// 请求失败(401):登录已过期

步骤 6:联合与交叉混合使用

interface BaseUser {
  id: number;
  name: string;
}

interface Admin {
  role: "admin";
  permissions: string[];
}

interface Member {
  role: "member";
  department: string;
}

// 交叉 + 联合:基础字段 × 角色判别联合
type User = BaseUser & (Admin | Member);

function describe(user: User): string {
  if (user.role === "admin") {
    return `${user.name} 是管理员,权限:${user.permissions.join(", ")}`;
  }
  return `${user.name} 是成员,部门:${user.department}`;
}

console.log(describe({ id: 1, name: "老板", role: "admin", permissions: ["all"] }));
// 老板 是管理员,权限:all
console.log(describe({ id: 2, name: "员工", role: "member", department: "技术部" }));
// 员工 是成员,部门:技术部

讲清楚要点

  1. | 是类型的并集:A | B 的值可以是 A 或 B;未收窄前只能访问两者共有的成员。
  2. 字面量联合优先于枚举:"a" | "b" | "c" 零编译产物、和后端字符串字段天然对齐,大多数场景替代 enum。
  3. 判别联合三要素:多个接口、一个共有的字面量判别字段(常用 kind/type/status)、一个联合类型把它们合起来。
  4. 判别字段必须是字面量类型:kind: string 无法判别(太宽),必须 kind: "loading" 这样精确到值。
  5. 交叉类型用于对象合并:& 叠加多个对象的字段;原始类型交叉基本只会得到 never,别这么写。
  6. 联合的分布式特性:对联合类型做操作时 TS 会逐个成员检查,这是后面条件类型和工具类型的基础。

你可能会懵的地方

  • A | B 类型上能直接访问 A 的独有属性吗? 不能。编译器不知道此刻它是 A 还是 B,只放行共有成员。必须先收窄(if、switch、typeof),让 TS 确定当前是哪一个。

  • 判别字段叫什么名字有讲究吗? 没有强制,社区惯用 kind、type、status。和后端对齐就行,关键是用字面量类型。

  • 联合类型最多能写几个成员? 语法上没有硬限制,但超过 7-10 个就该审视设计:是不是该用判别联合拆成多个子形状,或者某些成员该合并。

  • string | null 和 string | undefined 和 string? 有什么区别? 前两者是类型层面允许空值;? 只用于对象属性和函数参数,表示「可以缺省」。prop?: string 约等于 prop: string | undefined 且键可不存在。

  • 交叉类型字段冲突会怎样? type X = { a: string } & { a: number } 中 a 变成 string & number 即 never——没有值能满足。字段冲突时优先用接口继承(冲突当场报错)而非交叉。

错法对照

错法问题正解
status: string 表示固定状态集任意字符串都合法,笔误不报错字面量联合 "pending" | "paid"
联合类型直接调独有方法编译报错先 typeof/in/判别字段收窄
用可选属性堆叠表达多形态 {data?, error?}data 和 error 可同时存在或同时不存在,失去约束判别联合:成功必有 data,失败必有 error
string & number 期待「既是又是」结果是 never交叉只用于对象形状合并
判别字段写成 kind: string无法收窄每个分支用精确字面量 kind: "loading"

卡点急救

  1. 报错 Property 'x' does not exist on type 'A | B':你在访问非共有成员。确认是否已收窄;若逻辑上确定是某分支,用判别字段或 as 断言(后者要负责)。
  2. switch 收窄不生效:判别字段不是字面量类型(比如标成了 string),回接口里把 kind 改成精确字面量。
  3. 联合类型赋值报不兼容:值不符合联合中任何一个成员的形状。对照每个成员检查缺了哪个字段、哪个字段类型不对。
  4. A & B 推导出 never:两个类型的同名字段冲突。改用接口继承看冲突在哪,或重命名字段。

面试常问

  • 联合类型和交叉类型的区别? 联合 |:值是成员类型之一,取值范围是并集,访问能力限于共有成员;交叉 &:值要同时满足所有成员,主要用于对象形状的合并叠加。

  • 什么是判别联合?解决什么问题? 多个对象类型共享一个字面量判别字段,联合后 TS 可依据判别字段自动收窄到具体成员。解决「同一字段有多种形态数据」的建模问题,典型场景:API 成功/失败响应、UI 加载状态机、Redux 的 action。

  • type A = string | number 和 let a: string | number 能调什么方法? 只能调 string 和 number 共有的方法(如 toString、valueOf)。独有方法(toUpperCase、toFixed)必须先收窄。

  • 字面量联合和枚举怎么选? 现代 TS 倾向字面量联合:零运行时产物、与 JSON 数据直接兼容、tree-shaking 友好。枚举的优势是反向映射和集中命名空间,适合需要运行时遍历取值的场景。

自测三问

  1. 为什么 A | B 类型的值不能直接调用 A 独有的方法?怎么才能调?
  2. 判别联合的三个必要组成部分是什么?
  3. 「成功响应有 data、失败响应有 error、两者不会同时出现」——用可选属性和用判别联合建模,哪个更安全?为什么?

小结一句话

| 表达「多选一」、& 表达「全都要」、字面量联合圈定取值、判别联合让编译器替你把关每个分支——掌握这四样,业务数据建模就通关了。

延伸

  • Handbook: Union Types
  • 下一课:类型收窄——typeof、instanceof、in、类型守卫、断言函数、never 穷尽检查,把联合类型缩到精确形状。
加载本节练习…
← 上一课接口与类型别名下一课 →类型收窄