前置知识: TypeScript

交叉类型与类型合并

3 minIntermediate2026/6/14

交叉类型、接口合并与类型覆盖

概述

交叉型(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 }