当在 TypeScript 代码中需要表达
这个或那个时,我们需要联合类型。当需要表达这个且那个时,我们需要交叉类型。但实际使用中,90%的开发者对它们的理解都只停留在表面。本篇文章将详细讲解这两个核心类型操作符。
联合类型
联合类型的基本概念
联合类型(Union Types)用 | 操作符表示,允许一个值属于多种类型中的一种。举个生活中的小例子,就像我们去奶茶店买奶茶:
- 中杯 或 大杯 或超 大杯
- 热饮 或 冰饮
- 加糖 或 不加糖
这里的"或"就是联合类型的思想:多个选项中选一个,不能超过这些选项的选项值。上述奶茶中,我们想买个 小杯 可以吗?答案是不可以的,店员会告诉你:对不起先生,我们只有中杯、大杯、超大杯。
// 最简单的联合类型
type ID = string | number;
// 使用
let userId: ID;
userId = "abc123"; // ✅
userId = 123; // ✅
userId = true; // ❌ 类型错误
// 字面量联合类型:有限的值集合
type Size = "Mdeium" | "Big" | "Plus" ;
let size: Size;
size = "Mdeium"; // ✅
size = "small"; // ❌ 类型错误
类型守卫:告诉TypeScript当前是哪个类型
在使用联合类型时,会存在一个问题:TypeScript不知道现在是哪个具体类型。因此我们需要类型守卫来"收窄"类型。
属性值类型守卫
即:根据属性值进行判断,针对不同的属性值进行不同的操作,这是最常用的类型守卫:
function processState(state: UserState) {
switch (state.status) {
case "loggedIn":
console.log(state.user.name); // 安全访问
break;
case "error":
console.log(state.message); // 安全访问
break;
}
}
typeof & instanceof 类型守卫
这两者本质是一样的,即:通过属性值的实际类型进行判断,针对不同类型进行不同的操作:
typeof:
// typeof
function process(value: string | number) {
if (typeof value === "string") {
// 这里value被收窄为string
console.log(value.toUpperCase());
} else {
// 这里value被收窄为number
console.log(value.toFixed(2));
}
}
instanceof:
// instanceof
class ApiError extends Error {
constructor(message: string, public code: number) {
super(message);
}
}
class NetworkError extends Error {
constructor(message: string, public status: number) {
super(message);
}
}
function handleError(error: ApiError | NetworkError) {
if (error instanceof ApiError) {
// error被收窄为ApiError
console.log(`API错误 ${error.code}: ${error.message}`);
} else if (error instanceof NetworkError) {
// error被收窄为NetworkError
console.log(`网络错误 ${error.status}: ${error.message}`);
}
}
in 操作符守卫
即:通过 in 关键字,判断是否包含某个属性,针对不同的属性进行不同的操作:
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function play(pet: Cat | Dog) {
if ("meow" in pet) {
pet.meow(); // pet是Cat
} else {
pet.bark(); // pet是Dog
}
}
可辨识联合(Discriminated Unions)
这应该是TypeScript最强大的模式之一,在后面的章节中会单独讲解。
自定义类型守卫
当内置守卫不够用时,我们可以创建自己的守卫:
// 当内置守卫不够用时,创建自己的守卫
interface Cat {
meow(): void;
purr(): void;
}
interface Dog {
bark(): void;
wagTail(): void;
}
type Pet = Cat | Dog;
// 自定义类型守卫函数
function isCat(pet: Pet): pet is Cat {
return (pet as Cat).meow !== undefined;
}
function isDog(pet: Pet): pet is Dog {
return (pet as Dog).bark !== undefined;
}
function playWithPet(pet: Pet) {
if (isCat(pet)) {
pet.meow();
pet.purr();
} else {
pet.bark();
pet.wagTail();
}
}
可辨识联合(Discriminated Unions)
在本节开始之前,我们先看一个普通联合类型的问题:
// 普通联合类型
interface Circle {
radius: number;
}
interface Square {
sideLength: number;
}
interface Triangle {
base: number;
height: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape): number {
// 问题:如何知道shape是哪个类型?
if ("radius" in shape) {
// 不安全!如果有其他形状也有radius属性怎么办?
return Math.PI * shape.radius ** 2;
} else if ("sideLength" in shape) {
return shape.sideLength ** 2;
} else {
// 这是Triangle吗?不确定!
return (shape as any).base * (shape as any).height / 2;
}
}
上述代码存在以下几个问题:
- 类型守卫不精确
- 需要类型断言
- 容易出错
针对这么问题要怎么处理呢?TypeScript给出了可辨识联合的解决方案:
// 可辨识联合:给每个类型添加一个共同的"标签"字段
interface Circle {
kind: "circle"; // 标签:明确标识这是圆形
radius: number;
}
interface Square {
kind: "square"; // 标签:明确标识这是正方形
sideLength: number;
}
interface Triangle {
kind: "triangle"; // 标签:明确标识这是三角形
base: number;
height: number;
}
type Shape = Circle | Square | Triangle;
function getArea(shape: Shape): number {
// 现在可以通过kind字段精确判断
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // TypeScript知道这是Circle
case "square":
return shape.sideLength ** 2; // TypeScript知道这是Square
case "triangle":
return (shape.base * shape.height) / 2; // TypeScript知道这是Triangle
}
}
什么叫"可辨识"?
所谓可辨识,即:每个类型都有一个共同的字段:标签字段(通常是字面量类型),这个字段就像"身份证",能让 TypeScript 能够准确识别当前是哪个类型,如同上述代码中的 kind 属性。
标签字段的选择原则
任意字段都可以作为标签字段吗?当然不是,标签字段通常具备以下几个特性:
- 使用简单的字符串字面量,如:
type: string这种就是不合理的,不是字面量,无法辨识。 - 字段名一定要有意义:type、kind、status、role等。
- 每个类型的标签值要唯一。
联合类型的常见陷阱
过度使用any
type BadUnion = string | number | any; // 有any的联合类型等于any!
忘记处理所有情况
type Direction = "north" | "south" | "east" | "west";
function handleDirection(direction: Direction) {
switch (direction) {
case "north":
return "向上";
case "south":
return "向下";
case "east":
return "向右";
// 忘记处理"west"!
}
}
联合类型与数组的交互
// 这表示数组中的每个元素可以是string或number
type StringOrNumberArray = (string | number)[];
// 这表示要么整个数组是string[],要么整个数组是number[]
type StringArrayOrNumberArray = string[] | number[];
交叉类型
交叉类型的基本概念
交叉类型(Intersection Types)用 & 操作符表示,要求一个值同时满足多个类型的约束。还是回到上述奶茶店的例子,现在我们需要一杯更完整的奶茶:
- 有椰果 且 加奶盖 且 要吸管
这里的"且"就是交叉类型的思想:必须同时满足多个条件,缺一不可:
// 定义:用 & 连接多个类型
type HasCocount = { coconut: boolean };
type Cream = { cream: string };
type Straw = { straw: string };
// 必须同时有椰果、奶盖和吸管
type Combo = HasCocount & Cream & Straw;
// 使用:必须包含所有属性
const myOrder: Combo = {
coconut: true,
cream: "冰淇淋奶盖",
straw: "塑料吸管"
}; // ✅ 正确
const badOrder: Combo = {
coconut: true
// ❌ 错误:缺少cream和straw
};
交叉类型的常见陷阱
交叉类型不是"交集"
很多人误以为交叉类型是取交集,其实不是!我们来看一个简单的例子:
type A = { name: string; age: number };
type B = { age: string; city: string };
type C = A & B;
在类型 C 中,name 来自A,是 string ;city 来自B,是 string。那 age 呢?age 来自A和B,因此它的类型是既要 number ,又要 string ,这显然是不可能的。在 TypeScript 中,会将其推导为 never 类型,所以类型 C 的值永远无法创建:
const value1: C = {
name: "小明",
age: 25, // ❌ 不能将类型“number”分配给类型“never”
city: "北京"
};
const value2: C = {
name: "小明",
age: "25", // ❌ 不能将类型“string”分配给类型“never”
city: "北京"
};
联合类型 vs 交叉类型
简单对比
| 特性 | 联合类型 (|) | 交叉类型 (&) |
|---|---|---|
| 含义 | 或的关系:多个选项选一个 | 且的关系:必须同时满足 |
| 符号 | |(竖线) | &(和号) |
| 例子 | string | number | {x: number} & {y: number} |
| 适用场景 | 状态机、错误处理、配置选项 | 组合对象、扩展类型、Mixin模式 |
决策流程图
结语
本文主要介绍了联合类型与交叉类型的相关概念与对比,对于文章中错误的地方或者有任何问题,欢迎在评论区留言讨论!