交叉类型与类型合并
交叉类型、接口合并与类型覆盖
概述
交叉类型(Intersection Type)是 TypeScript 中组合多个类型的核心机制,使用 & 运算符将多个类型合并为一个类型。交叉类型要求值同时满足所有成员类型的约束,适用于混入(Mixin)、组合配置和类型增强等场景。理解交叉类型的行为,特别是属性冲突时的处理方式,对于编写类型安全的代码至关重要。此外,TypeScript 的接口声明合并和自定义类型覆盖工具也是类型组合的重要手段。
基础概念
交叉类型(&):将多个类型合并为一个类型,新类型拥有所有成员类型的属性。类似于集合的交集操作,值必须同时满足所有类型。
属性冲突:当交叉的多个类型中存在同名属性但类型不同时,该属性的类型会变为 never,表示不可能存在满足两种类型的值。
声明合并:TypeScript 中同名接口会自动合并其成员。这是接口独有的特性,类型别名不支持声明合并。
类型覆盖(Override):通过工具类型实现用一个类型覆盖另一个类型的部分属性,常用于修改已有类型的某些字段。
快速上手
基本交叉类型
// 组合多个类型
type Person = { name: string; age: number };
type Employee = { employeeId: number; department: string };
type EmployeePerson = Person & Employee;
const worker: EmployeePerson = {
name: '张三',
age: 30,
employeeId: 1001,
department: '工程部',
};
属性冲突处理
// 同名属性类型不同 -> 产生 never
type A = { prop: string };
type B = { prop: number };
type C = A & B;
// C 的 prop 类型为 never
// 因为不存在一个值既是 string 又是 number
const c: C = {
prop: 'hello' as never, // 必须使用类型断言
};
接口声明合并
// 同名接口自动合并
interface Box {
height: number;
width: number;
}
interface Box {
depth: number;
}
// Box = { height: number; width: number; depth: number }
const box: Box = { height: 10, width: 20, depth: 30 };
详细用法
Mixin 模式
// 使用交叉类型实现 Mixin
type Constructor<T = {}> = new (...args: any[]) => T;
// 时间戳 Mixin
function Timestamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
createdAt = new Date();
updatedAt = new Date();
};
}
// 软删除 Mixin
function SoftDeletable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
deletedAt: Date | null = null;
isDeleted = false;
softDelete() {
this.isDeleted = true;
this.deletedAt = new Date();
}
};
}
// 组合 Mixin
class User {
constructor(public name: string) {}
}
const TimestampedUser = Timestamped(User);
const FullUser = SoftDeletable(TimestampedUser);
// 类型推断:User & Timestamped & SoftDeletable
const user = new FullUser('张三');
user.name; // string
user.createdAt; // Date
user.isDeleted; // boolean
user.softDelete(); // 方法可用
类型覆盖工具
// Override:用 U 覆盖 T 中的同名属性
type Override<T, U> = Omit<T, keyof U> & U;
// 示例:修改 API 响应中的某些字段类型
interface APIUser {
id: string;
name: string;
createdAt: string; // API 返回的是字符串
updatedAt: string;
}
// 覆盖日期字段为 Date 类型
interface DateOverrides {
createdAt: Date;
updatedAt: Date;
}
type UIUser = Override<APIUser, DateOverrides>;
// { id: string; name: string; createdAt: Date; updatedAt: Date }
// 使用
function transformUser(api: APIUser): UIUser {
return {
...api,
createdAt: new Date(api.createdAt),
updatedAt: new Date(api.updatedAt),
};
}
条件属性合并
// 根据条件添加属性
type WithPagination<T, P extends boolean = true> = T &
(P extends true ? { page: number; pageSize: number; total: number } : {});
// 带分页的响应
interface UserList {
items: User[];
}
type PaginatedUserList = WithPagination<UserList, true>;
// { items: User[]; page: number; pageSize: number; total: number }
// 不带分页的响应
type SimpleUserList = WithPagination<UserList, false>;
// { items: User[] }
深度合并
// 深度合并两个类型
type DeepMerge<T, U> = {
[K in keyof T | keyof U]: K extends keyof T
? K extends keyof U
? T[K] extends object
? U[K] extends object
? DeepMerge<T[K], U[K]>
: U[K]
: U[K]
: T[K]
: K extends keyof U
? U[K]
: never;
};
// 示例
interface DefaultConfig {
api: { baseURL: string; timeout: number };
features: { darkMode: boolean };
}
interface UserConfig {
api: { timeout: number; retries: number };
features: { analytics: boolean };
}
type MergedConfig = DeepMerge<DefaultConfig, UserConfig>;
// {
// api: { baseURL: string; timeout: number; retries: number };
// features: { darkMode: boolean; analytics: boolean };
// }
常见场景
Props 组合
// React 组件 Props 组合
interface BaseProps {
className?: string;
style?: React.CSSProperties;
children?: React.ReactNode;
}
interface ButtonProps {
onClick: (e: React.MouseEvent) => void;
disabled?: boolean;
variant: 'primary' | 'secondary';
}
type EnhancedButtonProps = BaseProps & ButtonProps;
function Button({ className, style, children, onClick, disabled, variant }: EnhancedButtonProps) {
return (
<button
className={className}
style={style}
onClick={onClick}
disabled={disabled}
data-variant={variant}
>
{children}
</button>
);
}
配置合并
// 默认配置与用户配置合并
interface DefaultConfig {
port: number;
host: string;
debug: boolean;
logLevel: 'debug' | 'info' | 'warn' | 'error';
}
const defaultConfig: DefaultConfig = {
port: 3000,
host: 'localhost',
debug: false,
logLevel: 'info',
};
// 用户配置是默认配置的部分属性
type UserConfig = Partial<DefaultConfig>;
function mergeConfig(user: UserConfig): DefaultConfig {
return { ...defaultConfig, ...user };
}
// 类型安全的合并
const config = mergeConfig({ port: 8080, debug: true });
事件类型组合
// 组合多种事件的类型
interface ClickEvent {
type: 'click';
x: number;
y: number;
}
interface KeyEvent {
type: 'keydown' | 'keyup';
key: string;
code: string;
}
interface FocusEvent {
type: 'focus' | 'blur';
}
// 使用交叉类型扩展事件
type TrackedClickEvent = ClickEvent & { timestamp: number };
type TrackedKeyEvent = KeyEvent & { timestamp: number };
type TrackedEvent = TrackedClickEvent | TrackedKeyEvent;
function handleEvent(event: TrackedEvent) {
console.log(`事件时间: ${event.timestamp}`);
if (event.type === 'click') {
console.log(`点击位置: (${event.x}, ${event.y})`);
} else if (event.type === 'keydown' || event.type === 'keyup') {
console.log(`按键: ${event.key}`);
}
}
注意事项
- 属性冲突产生 never:交叉类型中同名属性类型不同时会产生 never。这是 TypeScript 的设计决策,表示不可能存在满足两种类型的值。编写代码时应避免这种情况。
- 接口合并 vs 交叉类型:接口合并是声明式的(同名接口自动合并),交叉类型是组合式的(显式使用 &)。接口合并在第三方库类型扩展中更常见,交叉类型在应用代码中更灵活。
- 性能考量:大量交叉类型(超过 10 个)可能导致编译器性能下降。对于复杂类型组合,考虑使用映射类型或工具类型替代。
- 函数类型的交叉:函数类型的交叉结果是重载,而非合并。调用时 TypeScript 会按顺序匹配重载签名。
- 循环引用:交叉类型可能导致循环类型引用,编译器会检测并报错。避免在交叉类型中直接引用自身。
进阶用法
严格属性覆盖
// StrictOverride:确保 U 的键是 T 的子集
type StrictOverride<T, U extends Partial<Record<keyof T, unknown>>> = Omit<T, keyof U> & U;
interface Original {
id: string;
name: string;
age: number;
}
// 正确:覆盖已有属性
type Modified1 = StrictOverride<Original, { id: number }>;
// { name: string; age: number; id: number }
// 错误:'email' 不是 Original 的属性
// type Modified2 = StrictOverride<Original, { email: string }>;
递归交叉类型
// 递归扩展嵌套对象
type RecursiveExtend<T, U> = T &
U & {
[K in keyof T & keyof U]: T[K] extends object
? U[K] extends object
? RecursiveExtend<T[K], U[K]>
: U[K]
: U[K];
};
// 示例:递归扩展配置
interface BaseTheme {
colors: {
primary: string;
secondary: string;
};
spacing: {
small: number;
large: number;
};
}
interface ThemeOverride {
colors: {
primary: string; // 覆盖
accent: string; // 新增
};
}
type ExtendedTheme = RecursiveExtend<BaseTheme, ThemeOverride>;
// colors: { primary: string; secondary: string; accent: string }
// spacing: { small: number; large: number }