跳到主要内容

联合类型与交叉类型

问题

什么是联合类型(Union Types)和交叉类型(Intersection Types)?它们有什么区别和应用场景?

答案

联合类型使用 | 表示"或"的关系,值可以是多种类型之一;交叉类型使用 & 表示"与"的关系,组合多个类型的所有成员。


联合类型(Union Types)

基本用法

// 基础联合类型
type StringOrNumber = string | number;

let value: StringOrNumber;
value = 'hello'; // OK
value = 42; // OK
// value = true; // 错误:不能将 boolean 分配给 StringOrNumber

// 字面量联合类型
type Status = 'pending' | 'success' | 'error';
type Digit = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9;

// 函数参数
function format(value: string | number): string {
if (typeof value === 'string') {
return value.toUpperCase();
}
return value.toFixed(2);
}

可辨识联合(Discriminated Unions)

使用共同属性区分类型:

interface Circle {
kind: 'circle';
radius: number;
}

interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}

interface Triangle {
kind: 'triangle';
base: number;
height: number;
}

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
case 'triangle':
return (shape.base * shape.height) / 2;
default:
// 穷尽检查:确保处理了所有情况
const _exhaustive: never = shape;
return _exhaustive;
}
}

收窄联合类型

type Value = string | number | boolean | null | undefined;

function process(value: Value) {
// typeof 收窄
if (typeof value === 'string') {
console.log(value.toUpperCase());
return;
}

// 真值检查收窄
if (value) {
// 排除 null、undefined、false
// value: string | number | true
}

// 相等性收窄
if (value === null) {
console.log('Value is null');
return;
}

// 此时 value: number | boolean | undefined
}

交叉类型(Intersection Types)

基本用法

interface Name {
firstName: string;
lastName: string;
}

interface Age {
age: number;
}

interface Address {
city: string;
country: string;
}

// 组合类型
type Person = Name & Age & Address;

const person: Person = {
firstName: 'John',
lastName: 'Doe',
age: 30,
city: 'Beijing',
country: 'China'
};

// 函数组合
type Loggable = {
log(): void;
};

type Serializable = {
serialize(): string;
};

type LoggableAndSerializable = Loggable & Serializable;

混入模式(Mixins)

// 定义能力
interface Flyable {
fly(): void;
altitude: number;
}

interface Swimmable {
swim(): void;
depth: number;
}

interface Walkable {
walk(): void;
speed: number;
}

// 组合能力
type Duck = Flyable & Swimmable & Walkable;

const duck: Duck = {
altitude: 100,
depth: 5,
speed: 3,
fly() { console.log('Flying...'); },
swim() { console.log('Swimming...'); },
walk() { console.log('Walking...'); }
};

与泛型结合

// 给任意类型添加时间戳
type Timestamped<T> = T & {
createdAt: Date;
updatedAt: Date;
};

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

type TimestampedUser = Timestamped<User>;
// { id: number; name: string; createdAt: Date; updatedAt: Date }

// 添加 ID
type WithId<T> = T & { id: string };

// 添加响应包装
type ApiResponse<T> = T & {
status: number;
message: string;
};

联合类型 vs 交叉类型

特性联合类型 |交叉类型 &
语义或(满足其一)与(满足全部)
对象属性取公共属性取所有属性
基本类型可以是任一类型可能产生 never
类型收窄需要不需要
使用场景值的多态类型组合

基本类型的行为

// 联合类型:可以是 string 或 number
type A = string | number;
const a1: A = 'hello'; // OK
const a2: A = 42; // OK

// 交叉类型:必须同时是 string 和 number(不可能)
type B = string & number; // never

// 接口交叉:属性合并
interface X {
a: string;
b: number;
}

interface Y {
b: number;
c: boolean;
}

type Z = X & Y;
// { a: string; b: number; c: boolean }

// 冲突属性
interface P {
x: string;
}

interface Q {
x: number;
}

type R = P & Q;
// x: string & number = never
// 对象类型本身不是 never,但 x 属性无法赋值

对象属性访问

interface Cat {
name: string;
meow(): void;
}

interface Dog {
name: string;
bark(): void;
}

// 联合类型:只能访问共同属性
type Pet = Cat | Dog;

function petName(pet: Pet) {
console.log(pet.name); // OK,name 是共同属性
// pet.meow(); // 错误:不是所有类型都有 meow
}

// 交叉类型:可以访问所有属性
type CatDog = Cat & Dog;

function catDogActions(catdog: CatDog) {
console.log(catdog.name); // OK
catdog.meow(); // OK
catdog.bark(); // OK
}

高级用法

条件类型与联合类型分发

// 联合类型在条件类型中会分发
type Wrap<T> = T extends any ? T[] : never;

type Result = Wrap<string | number>;
// (string | number)[] ❌ 错误理解
// string[] | number[] ✅ 实际结果(分发)

// 阻止分发
type WrapNoDistribute<T> = [T] extends [any] ? T[] : never;

type Result2 = WrapNoDistribute<string | number>;
// (string | number)[] ✅

过滤联合类型

// 排除类型
type Exclude<T, U> = T extends U ? never : T;

type Letters = 'a' | 'b' | 'c' | 'd';
type AorB = Exclude<Letters, 'c' | 'd'>; // 'a' | 'b'

// 提取类型
type Extract<T, U> = T extends U ? T : never;

type OnlyStrings = Extract<string | number | boolean, string>; // string

// 提取联合类型中的函数
type Mixed = string | number | (() => void) | ((x: number) => number);
type Functions = Extract<Mixed, (...args: any[]) => any>;
// (() => void) | ((x: number) => number)

将联合类型转换为交叉类型

// 高级技巧:Union to Intersection
type UnionToIntersection<U> = (
U extends any ? (k: U) => void : never
) extends (k: infer I) => void
? I
: never;

type Union = { a: string } | { b: number } | { c: boolean };
type Intersection = UnionToIntersection<Union>;
// { a: string } & { b: number } & { c: boolean }

获取联合类型成员数量

// 联合类型转元组(特殊技巧)
type UnionToTuple<T, L = LastInUnion<T>> = [T] extends [never]
? []
: [...UnionToTuple<Exclude<T, L>>, L];

type LastInUnion<T> = UnionToIntersection<
T extends any ? (x: T) => void : never
> extends (x: infer L) => void
? L
: never;

type Colors = 'red' | 'green' | 'blue';
type ColorTuple = UnionToTuple<Colors>; // ['red', 'green', 'blue']

实际应用场景

状态机

type State =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: string }
| { status: 'error'; error: Error };

function reducer(state: State, action: { type: string }): State {
switch (state.status) {
case 'idle':
return { status: 'loading' };
case 'loading':
// 可以转换到 success 或 error
return { status: 'success', data: 'loaded' };
case 'success':
console.log(state.data); // 安全访问 data
return state;
case 'error':
console.log(state.error); // 安全访问 error
return state;
}
}

API 响应处理

// 成功或失败响应
type ApiResult<T> =
| { success: true; data: T }
| { success: false; error: string };

async function fetchUser(id: number): Promise<ApiResult<User>> {
try {
const response = await fetch(`/api/users/${id}`);
const data = await response.json();
return { success: true, data };
} catch (e) {
return { success: false, error: (e as Error).message };
}
}

// 使用
const result = await fetchUser(1);
if (result.success) {
console.log(result.data.name); // 安全访问
} else {
console.error(result.error);
}

函数重载替代

// 使用联合类型代替函数重载
type DateInput = Date | string | number;

function parseDate(input: DateInput): Date {
if (input instanceof Date) return input;
if (typeof input === 'string') return new Date(input);
return new Date(input);
}

// 更复杂的场景
type QueryOptions =
| { type: 'single'; id: number }
| { type: 'multiple'; ids: number[] }
| { type: 'all' };

function query(options: QueryOptions) {
switch (options.type) {
case 'single':
return fetchOne(options.id);
case 'multiple':
return fetchMany(options.ids);
case 'all':
return fetchAll();
}
}

常见面试问题

Q1: 联合类型和交叉类型在对象上的区别?

答案

interface A {
a: string;
shared: number;
}

interface B {
b: boolean;
shared: number;
}

// 联合类型:值必须完全符合 A 或完全符合 B
type AorB = A | B;

const ab1: AorB = { a: 'hello', shared: 1 }; // OK (A)
const ab2: AorB = { b: true, shared: 2 }; // OK (B)
const ab3: AorB = { a: 'hello', b: true, shared: 3 }; // OK (满足 A 或 B)
// AorB 只能访问共同属性 shared

// 交叉类型:值必须同时满足 A 和 B
type AandB = A & B;

const aAndB: AandB = {
a: 'hello',
b: true,
shared: 1
}; // 必须有所有属性
// AandB 可以访问所有属性

Q2: 如何处理联合类型收窄?

答案

type Result = 
| { type: 'text'; content: string }
| { type: 'image'; url: string; alt: string }
| { type: 'video'; src: string; duration: number };

// 方法1:switch + 类型标签
function render(result: Result) {
switch (result.type) {
case 'text':
return renderText(result.content);
case 'image':
return renderImage(result.url, result.alt);
case 'video':
return renderVideo(result.src, result.duration);
}
}

// 方法2:in 操作符
function processResult(result: Result) {
if ('content' in result) {
// result: { type: 'text'; content: string }
console.log(result.content);
} else if ('url' in result) {
// result: { type: 'image'; url: string; alt: string }
console.log(result.alt);
} else {
// result: { type: 'video'; src: string; duration: number }
console.log(result.duration);
}
}

// 方法3:类型守卫函数
function isImage(result: Result): result is Extract<Result, { type: 'image' }> {
return result.type === 'image';
}

if (isImage(result)) {
console.log(result.url);
}

Q3: 什么情况下交叉类型会产生 never?

答案

// 基本类型交叉 -> never
type Impossible = string & number; // never
type Also = boolean & null; // never

// 冲突的对象属性
interface X { prop: string }
interface Y { prop: number }
type XY = X & Y;
// XY 不是 never,但 prop 是 never

const xy: XY = {
prop: 'hello' as never // 无法正确赋值
};

// 字面量冲突
type A = { status: 'active' };
type B = { status: 'inactive' };
type AB = A & B;
// AB.status = 'active' & 'inactive' = never

// 可兼容的交叉
type Good = { a: string } & { b: number }; // OK
type Also = string & { length: number }; // OK, string 有 length

Q4: 如何用联合类型实现可选属性的约束?

答案

// 场景:如果有 priceType,则必须有 price
// 基础方式无法实现这种依赖关系
interface Product {
name: string;
priceType?: 'fixed' | 'dynamic';
price?: number; // 无法强制:有priceType就必须有price
}

// 使用联合类型实现
type Product =
| {
name: string;
// 没有价格信息
}
| {
name: string;
priceType: 'fixed' | 'dynamic';
price: number; // 必须有
};

// 正确的使用
const p1: Product = { name: 'Item' }; // OK
const p2: Product = { name: 'Item', priceType: 'fixed', price: 100 }; // OK
// const p3: Product = { name: 'Item', priceType: 'fixed' }; // 错误:缺少 price

// 更复杂的依赖
type Request =
| { method: 'GET'; url: string }
| { method: 'POST'; url: string; body: object }
| { method: 'PUT'; url: string; body: object; id: string };

Q5: 类型分发是什么?如何阻止?

答案

// 类型分发:联合类型在条件类型中会逐个应用
type ToArray<T> = T extends any ? T[] : never;

type Result = ToArray<string | number>;
// 分发过程:
// ToArray<string> | ToArray<number>
// string[] | number[]

// 阻止分发:用元组包裹
type ToArrayNoDistribute<T> = [T] extends [any] ? T[] : never;

type Result2 = ToArrayNoDistribute<string | number>;
// (string | number)[]

// 实际应用
// 判断是否为联合类型(利用分发特性)
type IsUnion<T, U = T> = T extends any
? [U] extends [T]
? false
: true
: never;

type Test1 = IsUnion<string>; // false
type Test2 = IsUnion<string | number>; // true

相关链接