TypeScript技术系列7:联合类型和交叉类型希望通过本文的介绍,你对联合类型和交叉类型有了更深入的了解,并能够在今
前言
在前面的课程中,我们学习了TypeScript
中的基本类型、字面量类型、函数类型和接口类型。它们构建了我们理解TypeScript
类型系统的基础,帮助我们定义单一的、原子的类型。然而,在实际开发中,许多场景需要将这些基本类型组合在一起,以表达更加复杂的数据结构。这时候,联合类型(Union Types) 和 交叉类型(Intersection Types) 就派上了用场。
在这篇文章中,我们将深入探讨联合类型和交叉类型,理解它们的概念和用法,并通过多个实例来展示它们的实际应用。
1 联合类型(Union Types)
1.1 什么是联合类型?
联合类型允许我们定义一个变量可以是多个类型中的一种。通过使用|运算符,我们可以将不同的类型组合在一起。这非常适合用于处理那些输入类型不确定的情况,比如某个值可能是string
或number
。示例如下:
function processValue(value: string | number) {
if (typeof value === "string") {
console.log(`String value: ${value}`);
} else {
console.log(`Number value: ${value}`);
}
}
processValue("Hello"); // 输出: String value: Hello
processValue(42); // 输出: Number value: 42
在上面的例子中,processValue
函数接受一个参数,该参数可以是字符串或数字。在函数体内,使用typeof
操作符来判断传入的参数类型,并根据不同的类型进行不同的处理。
1.2 为什么使用联合类型?
使用联合类型的一个主要原因是它能够更好地表达代码的意图。在不使用联合类型的情况下,可能会不得不使用any
或unknown
类型来处理多种类型的输入,这样会导致类型系统失去强约束,增加了错误的可能性。而使用联合类型,可以确保输入的值属于一组特定的类型,同时保留了类型检查的优势。
1.3 联合类型的常见场景
1.3.1 参数的多种可能类型
在开发中,函数的参数常常可能是多种类型的。联合类型可以帮助我们在函数参数声明中明确这一点。示例如下:
function formatSize(size: number | string) {
if (typeof size === 'number') {
return `${size}px`;
}
if (typeof size === 'string') {
return `${parseInt(size, 10)}px`;
}
return 'Invalid size';
}
console.log(formatSize(15)); // 输出: 15px
console.log(formatSize('20px'));// 输出: 20px
在这个例子中,size
参数可以是number
或者string
。函数会根据类型分别处理数值和字符串。
1.3.2 联合类型中的类型缩减
联合类型不仅仅适用于简单的基础类型,也可以在复杂对象类型中发挥作用。在TypeScript
中,如果一个联合类型的多个成员共享相同的属性,我们可以直接访问这些共有的属性,而不需要进行类型断言。这种行为被称为类型缩减。示例如下:
interface Dog {
bark: () => void;
}
interface Cat {
meow: () => void;
}
function makeNoise(animal: Dog | Cat) {
if ("bark" in animal) {
animal.bark();
} else {
animal.meow();
}
}
const dog: Dog = { bark: () => console.log('Woof!') };
const cat: Cat = { meow: () => console.log('Meow!') };
makeNoise(dog); // 输出: Woof!
makeNoise(cat); // 输出: Meow!
在这个例子中,makeNoise
函数接收一个可能是Dog
或Cat
类型的对象。通过检查是否存在bark
属性,我们可以识别animal
是哪种类型,并调用相应的方法。
1.4 使用字面量联合类型
联合类型不仅可以用来组合基础类型,还可以用来组合字面量类型。这在定义更加具体的约束条件时非常有用。示例如下:
type Unit = 'px' | 'em' | 'rem';
function setUnit(value: number, unit: Unit) {
return `${value}${unit}`;
}
console.log(setUnit(10, 'px')); // 输出: 10px
console.log(setUnit(5, 'em')); // 输出: 5em
// console.log(setUnit(8, 'pt')); // 错误: 类型“"pt"”不能赋值给类型“Unit”
这里的Unit
类型是由'px' | 'em' | 'rem'
组成的字面量类型联合。当我们调用setUnit
时,只能传入这些特定的字符串作为单位值,否则TypeScript
会报错。
1.5 类型别名与联合类型
当联合类型非常复杂时,使用类型别名可以提高代码的可读性和可维护性。示例如下:
type NumberOrString = number | string;
function printValue(value: NumberOrString) {
console.log(value);
}
printValue(100); // 输出: 100
printValue("test"); // 输出: test
通过类型别名,我们可以轻松复用复杂的联合类型定义,而不必在多个地方重复编写相同的类型表达式。
2 交叉类型(Intersection Types)
2.1 什么是交叉类型?
交叉类型允许我们将多个类型合并成一个类型。使用&
运算符,我们可以创建一个新的类型,该类型同时具备多个类型的所有属性。这在需要组合多个接口或类型时非常有用。示例如下:
interface Person {
name: string;
}
interface Employee {
employeeId: number;
}
type EmployeePerson = Person & Employee;
const john: EmployeePerson = {
name: "John Doe",
employeeId: 1234
};
console.log(john);
在这个例子中,EmployeePerson
类型是通过将Person
和Employee
类型合并得到的,意味着它需要同时具备name
和employeeId
属性。
2.2 交叉类型的应用场景
2.2.1 组合多个接口
交叉类型最常见的应用场景是将多个接口合并成一个复合类型。例如,当我们需要描述一个对象,它既是某种类型的集合成员,又具备额外的属性时,可以使用交叉类型。示例如下:
interface Admin {
privileges: string[];
}
interface User {
name: string;
email: string;
}
type AdminUser = Admin & User;
const adminUser: AdminUser = {
privileges: ['server', 'network'],
name: 'Alice',
email: 'alice@example.com'
};
console.log(adminUser);
在这个例子中,AdminUser
既是一个Admin
,又是一个User
,因此它同时拥有privileges
、name
和email
属性。
2.2.2 处理对象的多个来源
当一个对象从多个数据源中获取属性时,交叉类型非常有用。假设我们有多个函数返回部分属性,可以使用交叉类型将它们组合在一起。示例如下:
function getPerson() {
return { name: "Bob" };
}
function getJob() {
return { jobTitle: "Developer" };
}
const fullProfile: Person & { jobTitle: string } = {
...getPerson(),
...getJob()
};
console.log(fullProfile); // 输出: { name: 'Bob', jobTitle: 'Developer' }
通过使用交叉类型和对象扩展运算符,可以组合来自不同函数的数据。
2.3 交叉类型中的类型冲突
当交叉类型中的成员具有相同的属性但类型不同,会发生冲突。此时,TypeScript
会将这些冲突的属性类型推断为never
,因为没有一种值可以同时满足两种冲突类型。示例如下:
type TypeA = { value: string };
type TypeB = { value: number };
type Conflicted = TypeA & TypeB;
const conflictedObject: Conflicted = {
value: 'test' // 错误: 类型“string”不能赋值给类型“never”
};
在这个例子中,Conflicted
类型中的value
属性既要求是string
又要求是number
,这显然是矛盾的,因此它的类型被推断为never
,导致我们无法为其赋值。
2.4 合并接口类型中的同名属性
在交叉类型中,如果两个接口具有相同的属性名,且属性类型是兼容的,则最终属性类型将是它们的子类型。示例如下:
interface Alpha {
value: string;
}
interface Beta {
value: string | number;
}
type Combined = Alpha & Beta;
const combinedObject: Combined = {
value: "Hello"
};
console.log(combinedObject);
在这个例子中,Alpha
和Beta
都有一个value
属性,但它们的类型是兼容的,因此在交叉类型Combined
中,value
的类型被推断为更严格的string
,因为这是两个类型的共同类型。
3 联合类型与交叉类型的结合应用
在实际项目中,联合类型和交叉类型常常结合使用,以表达复杂的数据结构或函数签名。示例如下:
interface ErrorState {
errorCode: number;
message: string;
}
interface LoadingState {
loading: boolean;
}
type NetworkState = ErrorState | LoadingState;
function handleState(state: NetworkState & { timestamp: Date }) {
if ('errorCode' in state) {
console.log(`Error: ${state.errorCode} - ${state.message}`);
} else {
console.log(`Loading: ${state.loading}, Timestamp: ${state.timestamp}`);
}
}
const errorState: NetworkState & { timestamp: Date } = {
errorCode: 404,
message: 'Not Found',
timestamp: new Date()
};
handleState(errorState);
在这个例子中,将联合类型NetworkState
和一个包含时间戳的对象类型结合在一起,形成一个新的交叉类型。这种模式常用于处理状态机或响应式数据结构。
总结
联合类型和交叉类型是TypeScript
中非常强大的功能,它们允许我们灵活地定义复杂的类型结构。通过联合类型,我们可以描述变量可能的多种类型,确保代码在处理不确定类型时的安全性;通过交叉类型,我们可以将多个类型合并在一起,创建具有更多属性的复合类型。
这两种类型在实际开发中广泛应用于处理多态数据、动态对象结构以及复杂的状态管理。理解并掌握它们的使用,将极大提高我们在大型项目中定义类型的灵活性和准确性。
希望通过本文的介绍,你对联合类型和交叉类型有了更深入的了解,并能够在今后的TypeScript
项目中灵活运用这些概念。
后语
小伙伴们,如果觉得本文对你有些许帮助,点个👍或者➕个关注再走吧^_^ 。另外如果本文章有问题或有不理解的部分,欢迎大家在评论区评论指出,我们一起讨论共勉。
转载自:https://juejin.cn/post/7415914052457611276