
在TypeScript的类型系统中,交叉类型(Intersection Types)和联合类型(Union Types) 是构建复杂类型表示的两大核心工具。它们提供了灵活的类型组合能力,让开发者能够精确描述各种数据结构。本文将深入探讨这两种类型的概念、区别以及实际应用场景。
一、联合类型(Union Types):"或"的关系
联合类型表示一个值可以是几个不同类型中的一个 ,使用 |
符号连接。
基本用法
ini
// 变量可以是字符串或数字
type StringOrNumber = string | number;
let value: StringOrNumber;
value = "hello"; // 有效
value = 42; // 有效
value = true; // 错误:布尔值不是允许的类型
// 函数参数可以是三种字符串字面量之一
type Status = "pending" | "approved" | "rejected";
function setStatus(status: Status) {
// ...
}
setStatus("approved"); // 有效
setStatus("deleted"); // 错误:不在允许的值中
类型守卫与联合类型
在使用联合类型时,TypeScript会限制只能访问所有类型共有的成员:
scss
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
type Animal = Bird | Fish;
function animalAction(animal: Animal) {
animal.layEggs(); // 有效:所有动物共有
animal.fly(); // 错误:可能不是所有动物都有
animal.swim(); // 错误:可能不是所有动物都有
}
要访问特定类型的成员,需要使用类型守卫:
scss
// 1. 使用类型断言
if ((animal as Bird).fly) {
(animal as Bird).fly();
}
// 2. 使用in运算符
if ('fly' in animal) {
animal.fly();
}
// 3. 用户自定义的类型守卫
function isBird(animal: Animal): animal is Bird {
return (animal as Bird).fly !== undefined;
}
if (isBird(animal)) {
animal.fly();
}
可辨识联合(Discriminated Unions)
当联合类型中的类型有共同的可辨识特征时,TypeScript能进行更精确的类型推断:
typescript
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; side: number }
| { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
// 此处shape自动被识别为圆
return Math.PI * shape.radius ** 2;
case "square":
// 此处shape自动被识别为正方形
return shape.side ** 2;
case "rectangle":
// 此处shape自动被识别为矩形
return shape.width * shape.height;
}
}
二、交叉类型(Intersection Types):"与"的关系
交叉类型表示一个类型同时具有多个类型的特性 ,使用 &
符号连接。
基本用法
typescript
interface Person {
name: string;
age: number;
}
interface Employee {
id: number;
department: string;
}
// 合并后的类型具有两种类型的所有属性
type EmployeePerson = Person & Employee;
const john: EmployeePerson = {
name: "John",
age: 30,
id: 123,
department: "Engineering"
};
交叉类型的类型组合
交叉类型在组合多个类型时非常灵活:
typescript
// 创建可序列化的错误类型
type SerializableError = Error & {
toJSON(): string;
};
// 具有只读和可写特性的混合数组
type ReadonlyWritableArray<T> = ReadonlyArray<T> & Array<T>;
三、关键区别:联合类型 vs 交叉类型
特性 | 联合类型 | 交叉类型 |
---|---|---|
符号 | ` | ` |
含义 | "或"关系 | "与"关系 |
值 | 可以是其中任意一个类型的值 | 必须同时满足所有类型的值 |
访问成员 | 只能访问共有成员 | 可以访问所有成员 |
类型检查 | 需要类型守卫确定具体类型 | 自动拥有所有特性 |
典型用途 | 函数参数多种可能、状态表示 | 组合多个接口、混合模式 |
理解区别的直观示例
ini
type A = { x: number };
type B = { y: string };
type C = { z: boolean };
// 联合类型:具有 x 或 y 或 z
type Union = A | B | C;
const u: Union = { x: 1 }; // 有效
const u2: Union = { y: "hi" }; // 有效
// 交叉类型:同时具有 x, y 和 z
type Intersection = A & B & C;
const i: Intersection = { // 必须包含所有属性
x: 1,
y: "hello",
z: true
};
四、高级用法与实际应用场景
1. 函数重载简化
使用联合类型简化重载声明:
typescript
// 传统重载写法
function getLength(obj: string): number;
function getLength(obj: any[]): number;
function getLength(obj: string | any[]): number {
return obj.length;
}
// 联合类型简化版
type StringOrArray = string | any[];
function getLength(obj: StringOrArray): number {
return obj.length;
}
2. 组合类型与条件类型
综合使用联合和交叉类型创建高级类型工具:
typescript
// 提取所有函数的返回类型
type ReturnTypes<T> =
T extends (...args: any[]) => infer R ? R : never;
// 创建非空类型
type NonNullable<T> = T extends null | undefined ? never : T;
// 交叉类型的条件判断
type ExtractProps<T> = T extends object ? keyof T : never;
// 混合多个对象属性
type Merge<A, B> = A & B & { [K in keyof A & keyof B]?: never };
3. React Props类型
在React中结合使用两种类型:
typescript
type ButtonProps = {
primary?: boolean;
size?: 'small' | 'medium' | 'large';
} & React.ButtonHTMLAttributes<HTMLButtonElement>;
const Button: React.FC<ButtonProps> = ({
primary,
size = 'medium',
...props
}) => {
// ...
};
五、实用技巧与最佳实践
1. 类型缩减
TypeScript会自动进行类型缩减:
typescript
// string & number 会自动缩减为 never
type Impossible = string & number; // => never
// boolean类型实际上是 true | false 的联合类型
type Boolean = true | false;
2. 联合类型的键访问
typescript
type User = { name: string; age: number } | { username: string; email: string };
// keyof (A | B) 是 keyof A 和 keyof B 的交集
type UserKeys = keyof User; // => never (无共同键)
function safeGet<K extends keyof User>(user: User, key: K) {
// 需要类型守卫确保安全
if (key in user) {
return user[key];
}
return undefined;
}
3. 使用类型别名提高可读性
css
// 复杂的类型组合可以拆解为多个别名
type ApiResponse<T> =
| { status: "success"; data: T }
| { status: "error"; message: string; code: number };
function handleResponse(response: ApiResponse<User>) {
// ...
}
4. 组合类型时的性能考虑
当交叉或联合大量复杂类型时,TypeScript的类型检查可能会变慢。此时可以使用接口继承替代交叉类型:
typescript
// 使用接口继承
interface Combined extends Person, Employee {
// 可以添加额外字段
role: string;
}
// 替代交叉类型
type Combined = Person & Employee & {
role: string;
};
六、常见问题解决
联合类型误用为交叉类型
ini
// 错误:以为可以接受同时具有两种接口的对象
type MyType = A | B;
const obj: MyType = { ...A, ...B }; // 可能错误,取决于类型定义
// 正确:使用交叉类型
type MyType = A & B;
处理可能的冲突字段
当交叉类型中的字段冲突时:
ini
type A = { id: number };
type B = { id: string };
// 冲突字段会变为 never 类型
type Conflict = A & B;
const c: Conflict = {
id: 5 // 错误:不能将number赋值给never
};
解决方案:使用类型映射处理冲突
less
type MergeAvoidingConflicts<A, B> = {
[K in keyof (A | B) as K extends keyof A ? (K extends keyof B ? never : K) : K]:
K extends keyof A ? A[K] : K extends keyof B ? B[K] : never;
};
七、何时使用哪种类型
场景 | 推荐类型 | 原因 |
---|---|---|
值可为多类型之一 | 联合类型(` | `) |
需要组合多个对象的属性 | 交叉类型(& ) |
合并类型特性 |
需要区分多种情况 | 联合类型 + 可辨识属性 | 类型守卫易用性 |
创建新类型扩展已有接口 | 交叉类型 | 无重复声明 |
描述状态机流转 | 联合类型 | 清晰表达互斥状态 |
混入多个类或对象 | 交叉类型 | 组合多个实现 |
函数参数多种可能 | 联合类型 | 灵活接受不同参数 |
实际开发建议
- 优先使用联合类型处理多种可能性场景
- 谨慎使用交叉类型组合复杂类型,防止意外合并
- 利用可辨识联合提高类型安全性
- 避免过度嵌套 - 复杂类型可拆分为多个别名
- 使用类型注释增强可读性 :
// type: 用户身份或访客
交叉类型和联合类型是TypeScript类型系统的两大支柱,掌握它们的特性和适用场景,能让你的类型定义更加精确灵活。理解何时选择何种类型,是提升TypeScript编程水平的重要一步。
"TypeScript的类型系统就像一个拼图游戏,交叉类型提供连接件,联合类型提供多种选择,共同构建出完整的类型图景。" - TypeScript设计理念