联合类型与交叉类型
问题
什么是联合类型(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