MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript联合类型与交叉类型的深入解析

2023-08-267.9k 阅读

TypeScript联合类型

在TypeScript中,联合类型(Union Types)是一种非常有用的类型系统特性,它允许一个变量或者函数参数等可以是多种类型中的一种。

联合类型的基本定义

联合类型使用竖线(|)来分隔不同的类型。例如,我们定义一个变量,它可以是字符串类型或者数字类型:

let myValue: string | number;
myValue = 'hello';
myValue = 42;

在上述代码中,myValue 变量被声明为 string | number 联合类型,所以它既可以赋值为字符串 'hello',也可以赋值为数字 42

联合类型在函数参数中的应用

联合类型在函数参数中经常被用到。比如,我们有一个函数,它可以接受字符串或者数字类型的参数,并将其打印出来:

function printValue(value: string | number) {
    console.log(value);
}

printValue('a string');
printValue(123);

这里 printValue 函数的参数 valuestring | number 联合类型,这使得函数更加灵活,可以处理不同类型的数据。

类型保护与联合类型

当使用联合类型时,有时候我们需要根据实际的类型来执行不同的逻辑。这就需要用到类型保护(Type Guards)。TypeScript提供了几种类型保护的方式,比如 typeof 类型保护。

function printValueInfo(value: string | number) {
    if (typeof value ==='string') {
        console.log(`The string length is ${value.length}`);
    } else {
        console.log(`The number squared is ${value * value}`);
    }
}

printValueInfo('test');
printValueInfo(5);

printValueInfo 函数中,通过 typeof 类型保护,我们可以在运行时判断 value 的实际类型,然后执行不同的逻辑。

另一种常见的类型保护是使用 instanceof。当联合类型中包含类类型时,instanceof 就很有用。假设我们有两个类 DogCat

class Dog {
    bark() {
        console.log('Woof!');
    }
}

class Cat {
    meow() {
        console.log('Meow!');
    }
}

function handlePet(pet: Dog | Cat) {
    if (pet instanceof Dog) {
        pet.bark();
    } else {
        pet.meow();
    }
}

const myDog = new Dog();
const myCat = new Cat();

handlePet(myDog);
handlePet(myCat);

这里 handlePet 函数通过 instanceof 类型保护,判断 petDog 还是 Cat,从而调用相应的方法。

联合类型与数组

联合类型也可以应用在数组中。例如,我们可以定义一个数组,它的元素可以是字符串或者数字:

let mixedArray: (string | number)[] = ['a', 1, 'b', 2];

这种数组在实际应用中可能不太常见,因为在操作数组元素时,我们需要时刻注意元素的类型。但在某些特定场景下,比如处理一些混合数据来源的数据时,这种联合类型数组就会很有用。

联合类型的运算

当对联合类型的值进行运算时,TypeScript会根据类型的交集来确定运算的结果类型。例如,对于 string | number 联合类型,如果进行加法运算:

let value1: string | number;
let value2: string | number;

let result = value1 + value2; 
// 这里 result 的类型是 string | number | undefined
// 因为如果 value1 是字符串,value2 是数字,加法运算结果是字符串拼接;如果两者都是数字,结果是数字相加;如果有一个未定义,结果可能是 undefined

如果要避免这种不确定性,我们需要在运算前通过类型保护来确定具体的类型。

TypeScript交叉类型

交叉类型(Intersection Types)是TypeScript中另一个强大的类型系统特性,它允许我们将多个类型合并为一个类型。

交叉类型的基本定义

交叉类型使用 & 符号来连接不同的类型。例如,我们定义一个类型,它同时具备 { name: string }{ age: number } 的属性:

type Person = { name: string } & { age: number };

let john: Person = { name: 'John', age: 30 };

在上述代码中,Person 类型是 { name: string }{ age: number } 的交叉类型,所以 john 对象必须同时具备 name 属性(字符串类型)和 age 属性(数字类型)。

交叉类型在接口中的应用

交叉类型在接口定义中也非常有用。假设我们有两个接口 EmployeeManager

interface Employee {
    name: string;
    salary: number;
}

interface Manager {
    department: string;
    manageTeam(): void;
}

type ManagerEmployee = Employee & Manager;

let tom: ManagerEmployee = {
    name: 'Tom',
    salary: 5000,
    department: 'Engineering',
    manageTeam() {
        console.log('Managing the team...');
    }
};

这里 ManagerEmployee 类型是 EmployeeManager 接口的交叉类型,所以 tom 对象需要满足 EmployeeManager 接口的所有属性和方法定义。

交叉类型与函数重载

交叉类型也可以与函数重载一起使用。例如,我们定义一个函数,它可以接受不同类型的参数并返回不同类型的结果,这些不同的参数和返回值类型可以通过交叉类型来组合:

function combine<T, U>(a: T, b: U): T & U {
    let result = {} as T & U;
    for (let key in a) {
        (result as any)[key] = a[key];
    }
    for (let key in b) {
        if (!Object.prototype.hasOwnProperty.call(result, key)) {
            (result as any)[key] = b[key];
        }
    }
    return result;
}

let obj1 = { name: 'Alice' };
let obj2 = { age: 25 };

let combined = combine(obj1, obj2);
// combined 的类型是 { name: string } & { age: number }
console.log(combined.name); 
console.log(combined.age); 

在这个 combine 函数中,它接受两个不同类型的参数 ab,并返回一个交叉类型 T & U 的结果,该结果包含了两个参数对象的所有属性。

交叉类型的继承与扩展

交叉类型也可以用于继承和扩展现有类型。比如,我们有一个基础类型 BaseType,然后通过交叉类型扩展它:

type BaseType = { id: number };

type ExtendedType = BaseType & { additionalInfo: string };

let myObject: ExtendedType = { id: 1, additionalInfo: 'Some extra info' };

这里 ExtendedTypeBaseType{ additionalInfo: string } 的交叉类型,扩展了 BaseType,使其包含了 additionalInfo 属性。

交叉类型与联合类型的对比

交叉类型和联合类型虽然都是将多个类型组合在一起,但它们有着本质的区别。联合类型表示一个值可以是多种类型中的一种,而交叉类型表示一个值必须同时满足多种类型的要求。

例如,string | number 联合类型意味着一个变量可以是字符串或者数字,而 { name: string } & { age: number } 交叉类型意味着一个对象必须同时有 name 字符串属性和 age 数字属性。

在实际应用中,联合类型常用于处理可能出现的不同类型数据,而交叉类型常用于将多个对象的属性或行为合并到一个类型中。

联合类型与交叉类型的高级应用

联合类型在泛型中的应用

在泛型中,联合类型可以使泛型更加灵活。例如,我们定义一个泛型函数,它可以接受一个数组,并返回数组中的第一个元素,但这个数组元素的类型可以是多种类型中的一种:

function getFirst<T extends string | number | boolean>(arr: T[]): T | undefined {
    return arr.length > 0? arr[0] : undefined;
}

let stringArray = ['a', 'b', 'c'];
let firstString = getFirst(stringArray); 

let numberArray = [1, 2, 3];
let firstNumber = getFirst(numberArray); 

let booleanArray = [true, false];
let firstBoolean = getFirst(booleanArray); 

这里 getFirst 函数的泛型参数 T 被限制为 string | number | boolean 联合类型,所以它可以处理不同类型元素的数组,并返回相应类型的第一个元素。

交叉类型在混入(Mixins)模式中的应用

混入(Mixins)模式是一种在面向对象编程中复用代码的方式,交叉类型在实现混入模式时非常有用。假设我们有几个基础类,每个类有不同的功能:

class Logger {
    log(message: string) {
        console.log(`LOG: ${message}`);
    }
}

class Timestamp {
    getTimestamp() {
        return new Date().getTime();
    }
}

function mixin<T, U>(base: new () => T, mixin: new () => U): new () => T & U {
    return class extends base {
        constructor() {
            super();
            const mix = new mixin();
            Object.assign(this, mix);
        }
    };
}

let LoggerWithTimestamp = mixin(Logger, Timestamp);

let instance = new LoggerWithTimestamp();
instance.log('Some log message');
console.log(instance.getTimestamp()); 

在上述代码中,mixin 函数接受两个类 basemixin,并返回一个新的类,这个新类是 basemixin 的交叉类型,即同时具备 Logger 类的 log 方法和 Timestamp 类的 getTimestamp 方法。

联合类型与交叉类型的嵌套

联合类型和交叉类型可以相互嵌套,形成更复杂的类型。例如,我们可以定义一个类型,它是一个联合类型,其中每个成员又是一个交叉类型:

type ComplexType = ({ name: string } & { age: number }) | ({ title: string } & { description: string });

let obj3: ComplexType = { name: 'Bob', age: 22 };
let obj4: ComplexType = { title: 'Article', description: 'This is an article' };

这里 ComplexType 是一个联合类型,它的成员分别是两个交叉类型。这种嵌套结构可以在处理非常复杂的数据结构时提供极大的灵活性。

联合类型和交叉类型在类型推断中的作用

在TypeScript的类型推断过程中,联合类型和交叉类型起着重要的作用。当一个表达式涉及联合类型时,TypeScript会根据具体的操作和上下文来推断最终的类型。例如:

let value3: string | number;
let result2 = value3 + 5; 
// 这里会报错,因为TypeScript无法确定 value3 的具体类型,不能直接与数字 5 相加

而对于交叉类型,TypeScript会要求对象必须满足所有交叉类型的属性和方法要求,在类型推断时会严格按照这个规则进行。

实际项目中的应用场景

联合类型的实际应用场景

  1. API响应数据处理:在前端开发中,从API获取的数据可能有不同的格式。例如,某个API可能返回一个对象,这个对象在不同情况下可能有不同的属性结构。我们可以使用联合类型来定义可能的返回类型。
type SuccessResponse = { status:'success'; data: any };
type ErrorResponse = { status: 'error'; message: string };

type APIResponse = SuccessResponse | ErrorResponse;

function handleAPIResponse(response: APIResponse) {
    if (response.status ==='success') {
        console.log('Data:', response.data);
    } else {
        console.log('Error:', response.message);
    }
}
  1. 用户输入处理:在表单输入中,用户可能输入不同类型的数据。比如,一个输入框可能接受数字或者字符串(例如,当输入的是表示数量的字符串时,也可以转换为数字处理)。
function processInput(input: string | number) {
    if (typeof input === 'number') {
        console.log('Number input:', input * 2);
    } else {
        console.log('String input:', input.length);
    }
}

交叉类型的实际应用场景

  1. 组件属性合并:在React等前端框架中,我们可能需要将多个不同的属性接口合并到一个组件的属性类型中。例如,一个组件可能既有通用的样式属性,又有特定业务逻辑的属性。
interface StyleProps {
    color: string;
    fontSize: number;
}

interface DataProps {
    data: any;
    onDataChange: () => void;
}

type ComponentProps = StyleProps & DataProps;

function MyComponent(props: ComponentProps) {
    // 组件逻辑
    return null;
}
  1. 对象功能扩展:当我们需要给一个现有的对象类型添加额外的功能时,可以使用交叉类型。例如,我们有一个基础的用户对象类型,然后需要添加一些管理员权限相关的属性。
type User = { name: string; age: number };
type AdminPrivileges = { isAdmin: boolean; canDeleteUser: () => void };

type AdminUser = User & AdminPrivileges;

let admin: AdminUser = {
    name: 'Admin User',
    age: 35,
    isAdmin: true,
    canDeleteUser() {
        console.log('Deleting user...');
    }
};

总结

联合类型和交叉类型是TypeScript类型系统中非常重要的特性,它们为开发者提供了强大的类型组合能力。联合类型适用于处理可能出现的多种类型数据,通过类型保护可以在运行时进行灵活处理;交叉类型则用于将多个类型的属性和行为合并到一个类型中,在对象功能扩展和属性合并等场景中非常实用。在实际项目中,合理运用联合类型和交叉类型可以提高代码的类型安全性和可维护性,使我们的代码更加健壮和清晰。无论是处理API响应、用户输入,还是在组件开发和对象功能扩展中,这两种类型都有着广泛的应用场景。开发者应该熟练掌握它们的使用方法,以充分发挥TypeScript类型系统的优势。

希望通过本文的详细介绍和丰富示例,读者能对TypeScript的联合类型和交叉类型有更深入的理解,并在实际开发中灵活运用,编写出高质量的TypeScript代码。