我终于真正理解TypeScript中交叉类型和联合类型的区别了

联合类型(Union Types)和交叉类型(Intersection Types)是 TypeScript 中的两种高级类型,它们都用于组合多个类型并生成新的类型,但它们两者之间的用法不一样。

1. 定义

1.1. 联合类型(|)

在TS中,联合类型表示:一个值可以是多种类型之一,使用逻辑"或"( | )运算符来分隔多个类型。

一个联合类型的变量,在使用时可以是多个类型中的任意一种。

typescript 复制代码
type UnionTypes = Type1 | Type2 | Type3;

1.2. 交叉类型(&)

在TS中,交叉类型表示:同时具备多种类型的值,使用逻辑"与"( & )运算符进行组合。

一个交叉类型的变量,将同时拥有多个类型的属性和方法。

typescript 复制代码
type IntersectionTypes = Type1 & Type2 & Type3;

2. 联合类型

2.1. 基础联合类型

当一个变量可以是多种不同的类型时,可以使用联合类型来定义它。

例如,一个变量可以是 string 类型或者 number 类型。

typescript 复制代码
let data: string | number;

data = 'hello ts';
data = 123;

data = false; // 编译错误:不能将类型"boolean"分配给类型"string | number"。ts(2322)

上面这段代码中,我们定义了一个变量 data,类型为 number 和 string 的联合类型,因此,data 的值只能是这两种类型中的其中一种,复制其它类型的值会报错。

2.2. 对象联合类型

对象联合类型只能访问联合中所有类型共有的成员。

typescript 复制代码
interface Admin {
  name: string;
  age: number;
}

interface User {
  name: string;
  sayHi(): void;
}

declare function Employee(): Admin | User;
let employee = Employee();
employee.name = 'Echo';

// 下面语句会报错:age属性不是 Admin 和 User 共有的属性
employee.age = 26; // 编译错误:类型"Admin | User"上不存在属性"age"。类型"User"上不存在属性"age"。ts(2339)

上面这段代码中,定义了两个接口 Admin 和 User,接着使用 declare function 声明了一个 Employee 函数,该函数的返回类型为 Admin 或 User。之后通过调用 Employee 函数并将返回值赋给了 employee 变量。接着将 employee 对象中 name 属性的值设置为 'Echo' 是可以的,因为 name 属性是 Admin 和 User 共有的属性。而将 employee 对象中 age 属性的值设置为 26 时会出现编译错误,错误信息指出类型 Admin | User 上不存在属性 age。这是因为 age 属性只存在于 Admin 接口中,而不属于 User 接口。

造成该错误的原因是,TypeScript 在联合类型上只能访问联合类型中所有类型的共有属性和方法。 因此,通过联合类型的变量只能访问 name 属性,而不能访问 age 属性。

2.3. 字面量联合类型

联合类型可以与字面量类型一起使用,用于限定一个值只能是某几个特定的值之一。

typescript 复制代码
let direction: "Up" | "Right" | "Down" | "Left";

direction = "Right";
direction = "none"; // 编译错误,只能取值为 "Up" | "Right" | "Down" | "Left"

3. 交叉类型

在 TypeScript 中,交叉类型(Intersection Types)允许我们将多个类型合并为一个新的类型。

使用交叉类型可以将多个对象的属性和方法合并到一个新的对象中。

typescript 复制代码
type Person = {
  name: string;
}

type User = {
  age: number;
}

let person: Person & User;

person = {
  name: 'Echo',
  age: 26,
}

// 编译错误:
// 不能将类型"{ name: string; }"分配给类型"Person & User"。
// 类型 "{ name: string; }" 中缺少属性 "age",但类型 "User" 中需要该属性。ts(2322)
// index.ts(7, 3): 在此处声明了 "age"。
person = {
  name: 'Steven',
}

上面这段代码中,我们定义了 Person 和 User 两个类型,然后,我们定义一个变量 person,它的类型是使用交叉类型 Person & User 来创建的一个新类型,那么,此时变量 person 就同时具备了 name 和 age 属性。

3.1. 交叉类型的成员类型是基础类型

交叉类型的成员类型可以为任意类型,但需要注意的是,如果交叉类型的成员类型是基础类型时,交叉类型的结果是 never。

typescript 复制代码
type T1 = string & number;   // 等同于 type T1 = never
type T2 = number & boolean;  // 等同于 type T2 = never

3.2. 交叉类型的成员类型是对象类型

当交叉类型的成员类型为对象类型时,结果类型又会是什么?

下面我们看一个简单的例子:

typescript 复制代码
type TypeA = {
  x: number;
  y: number;
}

type TypeB = {
  y: number;
  z: number;
}

type TypeC = {
  z: number;
}

上面这段代码中,我们定义了三个类型:TypeA、TypeB 和 TypeC,分别表示类型 A、B 和 C,类型 A 具有属性成员 x 和 y,类型 B 具有属性成员 y 和 z,类型 C 具有属性成员 z,每个类型具有不同的属性成员。

typescript 复制代码
type MergedType = TypeA & TypeB & TypeC;

上面这段代码中,我们使用交叉类型 TypeA & TypeB & TypeC 创建了一个新的类型 MergedType,它包含了类型 A、B 和 C 的属性成员,那么,合并后的交叉类型的成员类型为:属性成员 x 的类型是 A 的类型,属性成员 y 的类型是 A 和 B 的交叉类型,属性成员 z 的类型是 B 和 C 的交叉类型。

typescript 复制代码
let t: MergedType;

const t1 = {
  x: 1,
  y: 2,
  z: 3,
}

const t2 = {
  x: 10,
  y: 20,
}

t = t1;

// 编译错误:
// 不能将类型"{ x: number; y: number; }"分配给类型"MergedType"。
// 类型 "{ x: number; y: number; }" 中缺少属性 "z",但类型 "TypeB" 中需要该属性。ts(2322)
// index.ts(10, 3): 在此处声明了 "z"。
t = t2;

上面这段代码中,定义了一个变量 t,它的类型是 TypeA & TypeB & TypeC 组成的交叉类型,然后再定义了两个变量 t1 和 t2,t1 同时满足 TypeA、TypeB 和 TypeC 类型约束,因此能赋值给交叉类型 t。而 t2 满足 TypeA 类型约束,是 TypeA 类型,但并不能赋值给交叉类型 t,当 t2 赋值给 t 的时候,编译器会报错。

由此可见:交叉类型的类型成员由各个类型成员的属性成员的并集组成,并且这些属性成员的类型是各个成员类型的交叉类型。这种规则使得交叉类型能够将多个类型的属性成员合并到一个类型中,并且可以同时访问这些属性成员。

3.3. 成员类型合并

如果交叉类型的成员类型中有相同的类型,合并后的交叉类型将只保留一份该成员的类型。

typescript 复制代码
type T1 = string & string;   // 等同于 type T1 = string
type T2 = string & string & string;  // 等同于 type T2 = string

上面这段代码中,类型 T1 由两个 string 构成,由于成员类型相同,所以合并成为一个 string。类型 T2 由三个 string 构成,由于成员类型相同,所以合并成为一个 string。

3.4. 交叉类型的索引签名

当交叉类型的成员类型之一具有数字索引签名(即可通过数字索引访问)或字符串索引签名(即可通过字符串索引访问)时,结果类型也将包含相应的数字索引签名或字符串索引签名。

结果类型的索引签名值类型是各个成员类型索引签名值类型的交叉类型。也就是说,通过交叉类型合并的结果类型的索引签名值类型将是各个成员类型索引签名值类型的交叉类型。

typescript 复制代码
type TypeA = {
  [key: string]: string;
};

type TypeB = {
  [key: number]: string;
};

type MergedType = TypeA & TypeB;

const mergedObject: MergedType = {
  name: 'Echo',
  gender: 'Male',
  city: 'Guang Zhou',
  1: 'abcd',
};

console.log(mergedObject['name']);   // 输出:Echo
console.log(mergedObject['gender']); // 输出:Male
console.log(mergedObject['city']);   // 输出:Guang Zhou
console.log(mergedObject[1]);        // 输出:abcd

上面这段代码中,定义了两个类型 TypeA 和 TypeB,其中,TypeA 具有字符串索引签名,TypeB 具有数字索引签名,也就是说,TypeA 允许使用字符串作为索引,而 TypeB 允许使用数字作为索引。然后,使用交叉类型 TypeA & TypeB 创建了一个新的类型 MergedType,它包含了 TypeA 和 TypeB 的索引签名。接着,我们创建了一个名为 mergedObject 的对象,它的类型指定为交叉类型 MergedType,该对象可以通过数字索引或字符串索引来访问,并给这些索引赋予了相应的值。最后,我们通过索引访问 mergedObject 对象的值来验证交叉类型的索引签名的合并情况。

3.5. 交叉类型的调用签名

当交叉类型的成员类型中至少有一个具有调用签名时,交叉类型的结果类型也会包含这个调用签名。

换句话说,交叉类型中至少一个成员的调用签名会被合并到结果类型中。

此外,如果交叉类型的多个成员类型都有调用签名,那么结果类型将会形成调用签名重载的结构。调用签名重载允许我们为同一个函数提供多个不同的调用方式,具体取决于参数类型和返回值类型。

可以将交叉类型的成员类型的调用签名视为函数的签名,交叉类型的结果类型即为这些签名的合并。

typescript 复制代码
type FunctionA = (x: number, y: number) => number;
type FunctionB = (x: string, y: string) => string;

type FunctionType = FunctionA & FunctionB;

const option: FunctionType = (x, y) => x + y;

console.log(option(10, 20));     // 输出: 30
console.log(option('a', 'b'));   // 输出: ab

上面这段代码中,定义了两个类型 FunctionA 和 FunctionB,它们接收 x 和 y 两个参数,其中,FunctionA 两个参数的类型和函数返回值的类型都是 number 类型,FunctionB 两个参数的类型和函数返回值的类型都是 string 类型。然后,使用交叉类型 FunctionA & FunctionB 创建了一个新的类型 FunctionType,这个交叉类型包含了两个成员类型的调用签名。最后,我们创建了一个名为 option 的变量,它的类型被定义为 FunctionType,也就是 FunctionA 和 FunctionB 的交叉类型。我们可以使用 option 等同于调用两个函数的方式来执行相应的运算,option(10, 20) 相当于加法运算,输出结果为:30,option('a', 'b') 相当于字符串的拼接,输出结果为:ab。

3.6. 交叉类型的构造签名

当交叉类型的成员类型中至少有一个具有构造签名时,交叉类型的结果类型也会包含这个构造签名。

换句话说,交叉类型中至少存在一个成员的构造签名会被合并到结果类型中。

如果交叉类型的多个成员类型都具有构造签名,那么结果类型将形成构造签名重载的结构。构造签名重载允许我们为同一个类提供多个不同的构造方式,具体取决于参数列表。

typescript 复制代码
interface Foo {
  new (name: string): string
}
interface Bar {
  new (name: number): number;
}

type FooBar = Foo & Bar;

declare const T: FooBar;

const instance1 = new T('Echo');
const instance2 = new T(26);

上面这段代码中,我们定义了两个接口 Foo 和 Bar,它们都具有构造签名,分别接受不同的参数类型并返回对应的类型。接着,我们使用交叉类型 Foo & Bar 创建了一个新的类型 FooBar,它是 Foo 和 Bar 的交叉类型,意味着 FooBar 同时具备了 Foo 和 Bar 接口的构造签名。然后,通过 declare 关键字声明了一个常量 T,它的类型被定义为 FooBar。接着我们创建了两个实例 instance1 和 instance2。由于 T 的类型为 FooBar,我们可以使用 new T 的语法来实例化对象。对于 instance1,使用字符串 'Echo' 作为参数传递给构造函数,这符合 Foo 接口中定义的构造签名,所以实例化成功,返回一个字符串类型的实例。对于 instance2,使用数字 26 作为参数传递给构造函数,这符合 Bar 接口中定义的构造签名,所以实例化成功,返回一个数字类型的实例。

4. 总结

  • 联合类型只能访问共有的属性和方法。例如,如果一个变量是 number 类型或者 string 类型,那么只能使用这两种类型共有的属性和方法。
  • 联合类型中的变量,如果在特定条件下可以判断出其具体的类型,可以使用类型断言(as语法)来告诉编译器具体的类型。
  • 交叉类型的结果包含了所有成员类型的属性和方法,通过合并同名成员实现。属性会合并为并集类型,方法会合并为联合类型。
  • 当成员类型具有调用签名或构造签名时,交叉类型的结果将形成相应签名的重载。
相关推荐
吕彬-前端38 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱40 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai1 小时前
uniapp
前端·javascript·vue.js·uni-app
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js
微信:137971205872 小时前
web端手机录音
前端
齐 飞2 小时前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb