文章目录

TypeScript 交叉类型:A & B 类型合并

发布于 2026-04-06 01:24:57 · 浏览 13 次 · 评论 0 条

TypeScript 交叉类型:A & B 类型合并


什么是交叉类型

交叉类型是 TypeScript 中一种强大的类型组合方式,通过 & 符号将多个类型合并成一个新类型。这个新类型会拥有所有被合并类型的成员属性。想象一下,你有两张不同的蓝图,现在要把它们合并成一张综合蓝图——交叉类型做的事情正是如此。

在 TypeScript 的类型系统中,交叉类型代表的是「同时满足所有类型的要求」。与联合类型(用 | 表示「满足任意一个」)不同,交叉类型要求对象必须同时具备所有类型的特征。


基础用法

最简单的类型合并

假设你有两个接口类型,分别是 PersonEmployeePerson 包含姓名信息,Employee 包含工号信息。通过交叉类型,你可以创建一个同时包含这两者信息的新类型。

interface Person {
  name: string;
  age: number;
}

interface Employee {
  employeeId: string;
  department: string;
}

type EmployeeProfile = Person & Employee;

const employee: EmployeeProfile = {
  name: "张三",
  age: 30,
  employeeId: "E001",
  department: "技术部"
};

这段代码中,EmployeeProfilePersonEmployee 的交叉类型。赋值给 employee 的对象必须同时包含 nameageemployeeIddepartment 这四个属性,缺一不可。

与基本类型结合

交叉类型不仅可以用于接口,还可以与基本类型结合使用。当交叉类型中包含基本类型时,会产生 never 类型(不可达类型),因为一个值不可能同时是两种基本类型。

type Impossible = string & number;  // never 类型

但有一种特殊情况是基本类型与对象类型的交叉,这在实际开发中非常实用:

type ReadonlyUser = {
  id: number;
  name: string;
} & { readonly createdAt: Date };

const user: ReadonlyUser = {
  id: 1,
  name: "李四",
  createdAt: new Date()
};

与接口继承的区别

语法上的对比

在 TypeScript 中,扩展类型有两种常见方式:使用 extends 关键字的接口继承,以及使用 & 的交叉类型。两者看起来相似,但有细微差别。

// 方式一:接口继承
interface Admin extends User {
  adminLevel: number;
}

// 方式二:交叉类型
type Admin = User & {
  adminLevel: number;
};

这两种方式在简单的类型扩展场景下效果几乎相同,都能创建一个包含原类型所有成员的新类型。

核心差异:同名属性的处理

交叉类型与接口继承的关键区别在于「同名属性的合并规则」。接口继承中,子接口会完全覆盖父接口的同名属性;而交叉类型会根据属性类型进行智能合并。

interface A {
  value: string | number;
}

interface B {
  value: number;
}

// 接口继承:B.value 会完全覆盖 A.value,最终类型是 number
interface Inheritance extends A, B {
  value: number;
}

// 交叉类型:交叉后 value 是 string | number & number,最终类型是 number
type Intersection = A & B;

这个例子展示了交叉类型的智能合并特性。Avaluestring | numberBvaluenumber。当两者交叉时,TypeScript 会计算交集,结果是 number 类型。


实际应用场景

类型混入(Mixin)

在 JavaScript/TypeScript 中没有多继承,但交叉类型配合类型工具可以实现类似的效果。你可以把多个特征类型「混入」到一个最终类型中。

interface CanSwim {
  swim(): void;
}

interface CanFly {
  fly(): void;

interface CanRun {
  run(): void;
}

type Duck = CanSwim & CanFly & CanRun;

const duck: Duck = {
  swim() { console.log("游泳"); },
  fly() { console.log("飞翔"); },
  run() { console.log("奔跑"); }
};

这种方式让你可以像搭积木一样灵活组合类型特征,每个特征定义一组相关属性和方法,最终组合成完整的类型。

函数参数的灵活约束

当你需要一个函数接受多种配置组合时,交叉类型非常有用。它可以强制要求调用者提供所有必需的配置项,同时允许可选配置的灵活组合。

interface DatabaseConfig {
  host: string;
  port: number;
}

interface CacheConfig {
  enableCache: boolean;
  cacheSize: number;
}

type ServiceConfig = DatabaseConfig & CacheConfig;

function initializeService(config: ServiceConfig): void {
  // config 必须同时包含 DatabaseConfig 和 CacheConfig 的所有属性
  console.log(`连接数据库: ${config.host}:${config.port}`);
  console.log(`缓存大小: ${config.cacheSize}MB`);
}

// 正确调用
initializeService({
  host: "localhost",
  port: 3306,
  enableCache: true,
  cacheSize: 1024
});

扩展第三方库类型

当你使用第三方库时,经常需要为现有类型添加额外属性。交叉类型提供了一种安全的方式来扩展类型,而不需要修改原始类型定义。

import { Express } from "express";

// 扩展 Express 的 Request 类型
declare module "express" {
  interface Request {
    userId?: string;
    isAuthenticated: boolean;
  }
}

// 使用时
type AuthenticatedRequest = Request & {
  additionalData: {
    tokenVersion: number;
  }
};

高级技巧与注意事项

类型推断的优先级

在交叉类型中,infer 推导的优先级是从左到右的。理解这一点对于编写复杂的类型工具非常重要。

type First<T> = T extends infer R ? R : never;
type Second<T> = T extends (infer R)[1] ? R : never;

type Test = [string, number];

type A = First<Test>;  // [string, number]
type B = Second<Test>; // number

避免类型膨胀

过度使用交叉类型可能导致类型定义臃肿。最佳实践是保持类型的单一职责,每个类型只描述一个方面的特征。

// 不推荐:把所有属性堆在一个类型中
type Monster = {
  name: string;
  health: number;
  attack: number;
  defense: number;
  speed: number;
  magic: number;
};

// 推荐:按职责分离后交叉
type PhysicalStats = { attack: number; defense: number; speed: number; };
type MagicalStats = { magic: number; };
type Stats = PhysicalStats & MagicalStats;

type Entity = { name: string; health: number; } & Stats;

处理函数类型的交叉

当交叉类型包含函数时,需要注意函数参数的协变特性。TypeScript 默认使用协变(covariant)处理返回类型,逆变(contravariant)处理参数类型。

interface A {
  greet(name: string): void;
}

interface B {
  greet(name: "hello"): void;
}

// 交叉后的类型
type Greeter = A & B;

// 实际使用时,必须同时兼容两种调用方式
const greeter: Greeter = (name: string | "hello") => {
  console.log(name);
};

常见模式

必需属性与可选属性的合并

当你需要把可选类型转换为必需类型时,可以结合交叉类型实现:

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Required<T> = {
  [P in keyof T]-?: T[P];
};

// 将部分配置转换为完整配置
type FullConfig = {
  timeout: number;
  retries: number;
} & Required<Partial<{ debug: boolean; verbose: boolean }>>;

// FullConfig 等价于:
// {
//   timeout: number;
//   retries: number;
//   debug: boolean;
//   verbose: boolean;
// }

与映射类型的组合

交叉类型与映射类型结合可以实现强大的类型转换:

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

type Config = {
  name: string;
  items: string[];
};

// 创建只读版本
type FrozenConfig = Config & Readonly<{ debug: boolean; cache: boolean }>;

总结

交叉类型是 TypeScript 类型系统中不可或缺的工具。它通过 & 符号将多个类型无缝合并,让你可以像组合乐高积木一样灵活构建复杂的类型结构。掌握交叉类型的用法,你就能设计出更加类型安全、代码复用性更高的程序。

记住三个核心要点:交叉类型要求对象同时满足所有被合并类型的约束;同名属性会根据类型兼容性规则进行智能合并;过度使用会导致类型膨胀,保持适度是关键。

评论 (0)

暂无评论,快来抢沙发吧!

扫一扫,手机查看

扫描上方二维码,在手机上查看本文