likes
comments
collection
share

TypeScript技术系列7:联合类型和交叉类型希望通过本文的介绍,你对联合类型和交叉类型有了更深入的了解,并能够在今

作者站长头像
站长
· 阅读数 26

前言

在前面的课程中,我们学习了TypeScript中的基本类型、字面量类型、函数类型和接口类型。它们构建了我们理解TypeScript类型系统的基础,帮助我们定义单一的、原子的类型。然而,在实际开发中,许多场景需要将这些基本类型组合在一起,以表达更加复杂的数据结构。这时候,联合类型(Union Types) 和 交叉类型(Intersection Types) 就派上了用场。

在这篇文章中,我们将深入探讨联合类型交叉类型,理解它们的概念和用法,并通过多个实例来展示它们的实际应用。

1 联合类型(Union Types)

1.1 什么是联合类型?

联合类型允许我们定义一个变量可以是多个类型中的一种。通过使用|运算符,我们可以将不同的类型组合在一起。这非常适合用于处理那些输入类型不确定的情况,比如某个值可能是stringnumber。示例如下:

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 为什么使用联合类型?

使用联合类型的一个主要原因是它能够更好地表达代码的意图。在不使用联合类型的情况下,可能会不得不使用anyunknown类型来处理多种类型的输入,这样会导致类型系统失去强约束,增加了错误的可能性。而使用联合类型,可以确保输入的值属于一组特定的类型,同时保留了类型检查的优势。

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函数接收一个可能是DogCat类型的对象。通过检查是否存在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类型是通过将PersonEmployee类型合并得到的,意味着它需要同时具备nameemployeeId属性。

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,因此它同时拥有privilegesnameemail属性。

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);

在这个例子中,AlphaBeta都有一个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
评论
请登录