TypeScript 交叉类型:A & B 类型合并
什么是交叉类型
交叉类型是 TypeScript 中一种强大的类型组合方式,通过 & 符号将多个类型合并成一个新类型。这个新类型会拥有所有被合并类型的成员属性。想象一下,你有两张不同的蓝图,现在要把它们合并成一张综合蓝图——交叉类型做的事情正是如此。
在 TypeScript 的类型系统中,交叉类型代表的是「同时满足所有类型的要求」。与联合类型(用 | 表示「满足任意一个」)不同,交叉类型要求对象必须同时具备所有类型的特征。
基础用法
最简单的类型合并
假设你有两个接口类型,分别是 Person 和 Employee。Person 包含姓名信息,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: "技术部"
};
这段代码中,EmployeeProfile 是 Person 和 Employee 的交叉类型。赋值给 employee 的对象必须同时包含 name、age、employeeId 和 department 这四个属性,缺一不可。
与基本类型结合
交叉类型不仅可以用于接口,还可以与基本类型结合使用。当交叉类型中包含基本类型时,会产生 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;
这个例子展示了交叉类型的智能合并特性。A 的 value 是 string | number,B 的 value 是 number。当两者交叉时,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 类型系统中不可或缺的工具。它通过 & 符号将多个类型无缝合并,让你可以像组合乐高积木一样灵活构建复杂的类型结构。掌握交叉类型的用法,你就能设计出更加类型安全、代码复用性更高的程序。
记住三个核心要点:交叉类型要求对象同时满足所有被合并类型的约束;同名属性会根据类型兼容性规则进行智能合并;过度使用会导致类型膨胀,保持适度是关键。

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