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

Typescript中的交叉类型和联合类型

2023-05-197.7k 阅读

交叉类型

在 TypeScript 中,交叉类型(Intersection Types)是将多个类型合并为一个类型。这意味着一个对象可以同时拥有多个类型的特性。

交叉类型的语法

交叉类型使用 & 符号来表示。例如,如果我们有两个类型 TypeATypeB,我们可以创建一个交叉类型 TypeAB,它同时具有 TypeATypeB 的属性和方法。

type TypeA = {
    propertyA: string;
};

type TypeB = {
    propertyB: number;
};

type TypeAB = TypeA & TypeB;

let obj: TypeAB = {
    propertyA: 'Hello',
    propertyB: 42
};

在上述代码中,TypeABTypeATypeB 的交叉类型。obj 对象必须同时包含 propertyA(类型为 string)和 propertyB(类型为 number)。

交叉类型的应用场景

  1. 混入(Mixins)模式:在面向对象编程中,混入模式是一种将多个类的功能合并到一个类中的技术。在 TypeScript 中,我们可以使用交叉类型来模拟混入模式。
// 定义混入类型
type Loggable = {
    log(): void;
};

type Serializable = {
    serialize(): string;
};

// 创建一个同时具有 Loggable 和 Serializable 特性的类型
type LoggableSerializable = Loggable & Serializable;

// 实现 Loggable 混入
class ConsoleLogger implements Loggable {
    log(): void {
        console.log('Logging...');
    }
}

// 实现 Serializable 混入
class JsonSerializer implements Serializable {
    serialize(): string {
        return '{}';
    }
}

// 创建一个类,结合 Loggable 和 Serializable 特性
class MyClass implements LoggableSerializable {
    log(): void {
        console.log('MyClass logging...');
    }

    serialize(): string {
        return '{"message": "MyClass data"}';
    }
}

let myObj: MyClass = new MyClass();
myObj.log();
let serialized = myObj.serialize();

在这个例子中,MyClass 实现了 LoggableSerializable 交叉类型,因此它同时具有 logserialize 方法。

  1. 扩展现有类型:有时候我们需要在现有类型的基础上添加额外的属性或方法。交叉类型可以帮助我们做到这一点。
// 假设我们有一个内置的 DOM 元素类型
interface Element {
    id: string;
    tagName: string;
}

// 我们想为某些元素添加一个自定义的属性
type CustomElement = Element & {
    customData: any;
};

let customDiv: CustomElement = {
    id: 'div1',
    tagName: 'DIV',
    customData: { message: 'This is custom data' }
};

在这里,CustomElementElement 和一个包含 customData 属性的类型的交叉类型。这样我们就可以为 DOM 元素添加自定义的数据。

交叉类型与接口继承的区别

虽然接口继承和交叉类型都可以用于扩展类型,但它们有一些重要的区别。

  1. 语法不同:接口继承使用 extends 关键字,而交叉类型使用 & 符号。
// 接口继承
interface Animal {
    name: string;
}

interface Dog extends Animal {
    bark(): void;
}

// 交叉类型
type Cat = {
    name: string;
} & {
    meow(): void;
};
  1. 应用场景不同:接口继承更适合建立类型之间的层次结构,比如 DogAnimal 的一种特殊类型。而交叉类型更适合将多个不相关的类型组合在一起,比如 LoggableSerializable 并没有层次关系。

  2. 合并方式不同:当接口继承时,如果子接口和父接口有同名属性,子接口的属性会覆盖父接口的属性。而交叉类型中,如果两个类型有同名属性,那么属性的类型必须兼容。

// 接口继承中的属性覆盖
interface Parent {
    value: string;
}

interface Child extends Parent {
    value: number; // 这里会报错,因为 number 类型不能覆盖 string 类型
}

// 交叉类型中的属性兼容
type Type1 = {
    value: string;
};

type Type2 = {
    value: number;
};

// 这里会报错,因为 string 和 number 类型不兼容
type Type3 = Type1 & Type2;

联合类型

联合类型(Union Types)允许一个值具有多种类型中的一种。这在处理可能有不同类型的变量时非常有用。

联合类型的语法

联合类型使用 | 符号来表示。例如,如果一个变量可以是 string 类型或者 number 类型,我们可以定义如下联合类型。

let value: string | number;
value = 'Hello';
value = 42;

在上述代码中,value 变量可以被赋值为 string 类型的值或者 number 类型的值。

访问联合类型的属性和方法

当访问联合类型的属性或方法时,需要注意只有所有联合类型都有的属性和方法才能被安全访问。

function printValue(value: string | number) {
    // 这里只能访问 string 和 number 共有的属性和方法
    console.log(value.toString());
}

printValue('Hello');
printValue(42);

printValue 函数中,我们调用了 toString 方法,因为 stringnumber 类型都有 toString 方法。如果我们尝试访问 length 属性(只有 string 类型有),TypeScript 会报错。

function printLength(value: string | number) {
    // 这里会报错,因为 number 类型没有 length 属性
    console.log(value.length);
}

类型保护(Type Guards)

为了在联合类型中安全地访问特定类型的属性和方法,我们可以使用类型保护。类型保护是一种运行时检查,用于确定一个联合类型的值实际属于哪种类型。

  1. typeof 类型保护typeof 操作符可以用于检查一个值的类型。
function printValueWithLength(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else {
        console.log(value.toFixed(2));
    }
}

printValueWithLength('Hello');
printValueWithLength(42);

printValueWithLength 函数中,通过 typeof 类型保护,我们可以在 if 块内安全地访问 string 类型特有的 length 属性,在 else 块内安全地访问 number 类型特有的 toFixed 方法。

  1. instanceof 类型保护instanceof 操作符用于检查一个对象是否是某个类的实例。
class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

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

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

function makeSound(animal: Animal | Dog | Cat) {
    if (animal instanceof Dog) {
        animal.bark();
    } else if (animal instanceof Cat) {
        animal.meow();
    } else {
        console.log('The animal makes no sound.');
    }
}

let myDog = new Dog('Buddy');
let myCat = new Cat('Whiskers');
let myAnimal = new Animal('Generic Animal');

makeSound(myDog);
makeSound(myCat);
makeSound(myAnimal);

makeSound 函数中,通过 instanceof 类型保护,我们可以根据 animal 的实际类型调用相应的方法。

联合类型的应用场景

  1. 函数参数的多种类型:当一个函数可以接受不同类型的参数时,可以使用联合类型。
function logValue(value: string | number) {
    console.log(`The value is: ${value}`);
}

logValue('Hello');
logValue(42);
  1. 可选属性的不同类型:在对象类型中,某些属性可能是可选的,并且具有不同的类型。
interface Options {
    color?: string | number;
    size?: number;
}

function configure(options: Options) {
    if (options.color) {
        if (typeof options.color ==='string') {
            console.log(`Setting color to ${options.color}`);
        } else {
            console.log(`Setting color code to ${options.color}`);
        }
    }
    if (options.size) {
        console.log(`Setting size to ${options.size}`);
    }
}

let myOptions: Options = {
    color: 'red',
    size: 10
};

configure(myOptions);

Options 接口中,color 属性是可选的,并且可以是 string 类型或 number 类型。通过类型保护,我们可以在 configure 函数中正确处理不同类型的 color 值。

交叉类型与联合类型的组合使用

在实际编程中,交叉类型和联合类型常常组合使用。

复杂类型的构建

我们可以通过组合交叉类型和联合类型来创建非常复杂的类型。

// 定义一些基础类型
type Shape = {
    kind:'shape';
};

type Circle = Shape & {
    kind: 'circle';
    radius: number;
};

type Square = Shape & {
    kind:'square';
    sideLength: number;
};

// 定义一个联合类型,表示可能是圆形或正方形
type ShapeUnion = Circle | Square;

function calculateArea(shape: ShapeUnion) {
    if (shape.kind === 'circle') {
        return Math.PI * shape.radius * shape.radius;
    } else {
        return shape.sideLength * shape.sideLength;
    }
}

let circle: Circle = {
    kind: 'circle',
    radius: 5
};

let square: Square = {
    kind:'square',
    sideLength: 4
};

console.log(calculateArea(circle));
console.log(calculateArea(square));

在这个例子中,我们首先定义了 Shape 基础类型,然后通过交叉类型分别定义了 CircleSquare 类型。接着,我们使用联合类型 ShapeUnion 表示可能是圆形或正方形。在 calculateArea 函数中,通过类型保护来计算不同形状的面积。

处理复杂的对象结构

组合交叉类型和联合类型可以更好地描述复杂的对象结构。

// 定义用户类型
type User = {
    name: string;
    age: number;
};

// 定义管理员类型
type Admin = {
    name: string;
    age: number;
    isAdmin: true;
};

// 定义用户或管理员的联合类型
type UserOrAdmin = User | Admin;

// 定义一个包含用户或管理员列表的公司类型
type Company = {
    employees: UserOrAdmin[];
};

let myCompany: Company = {
    employees: [
        { name: 'Alice', age: 25 },
        { name: 'Bob', age: 30, isAdmin: true }
    ]
};

function printEmployeeInfo(employee: UserOrAdmin) {
    let info = `Name: ${employee.name}, Age: ${employee.age}`;
    if ('isAdmin' in employee) {
        info +='(Admin)';
    }
    console.log(info);
}

myCompany.employees.forEach(printEmployeeInfo);

在这个例子中,我们定义了 UserAdmin 类型,通过联合类型 UserOrAdmin 表示可能是用户或管理员。Company 类型的 employees 属性是 UserOrAdmin 的数组。在 printEmployeeInfo 函数中,通过 in 操作符作为类型保护来判断员工是否是管理员,并打印相应的信息。

类型别名与交叉、联合类型

在 TypeScript 中,类型别名(Type Aliases)常常与交叉类型和联合类型一起使用,以提高代码的可读性和可维护性。

使用类型别名定义交叉和联合类型

// 使用类型别名定义交叉类型
type Point = { x: number; y: number };
type Color = { color: string };
type ColoredPoint = Point & Color;

let myColoredPoint: ColoredPoint = { x: 10, y: 20, color:'red' };

// 使用类型别名定义联合类型
type StringOrNumber = string | number;
let myValue: StringOrNumber = 42;
myValue = 'Hello';

通过类型别名,我们可以为复杂的交叉类型和联合类型赋予一个有意义的名称,使得代码更易于理解。

类型别名在函数参数和返回值中的应用

// 使用类型别名定义函数参数和返回值类型
type NumberOrStringProcessor = (input: string | number) => string | number;

function processValue(input: string | number): string | number {
    if (typeof input ==='string') {
        return input.length.toString();
    } else {
        return input * 2;
    }
}

let processor: NumberOrStringProcessor = processValue;
let result1 = processor('Hello');
let result2 = processor(10);

在这个例子中,我们使用类型别名 NumberOrStringProcessor 来定义函数类型,该函数接受 stringnumber 类型的参数,并返回 stringnumber 类型的值。这样可以使函数的类型定义更加清晰。

交叉类型和联合类型在泛型中的应用

泛型(Generics)是 TypeScript 中非常强大的特性,交叉类型和联合类型在泛型中也有广泛的应用。

泛型与交叉类型

// 定义一个泛型函数,接受两个具有相同属性的对象,并返回它们属性的交叉类型
function merge<T, U>(obj1: T, obj2: U): T & U {
    return { ...obj1, ...obj2 };
}

let objA = { name: 'Alice' };
let objB = { age: 25 };
let mergedObj = merge(objA, objB);
// mergedObj 的类型是 { name: string; age: number; }

merge 函数中,通过泛型 TU 以及交叉类型 T & U,我们可以将两个不同类型的对象合并为一个具有两个对象所有属性的新对象。

泛型与联合类型

// 定义一个泛型函数,接受一个联合类型的值,并返回该值的字符串表示
function stringifyValue<T extends string | number | boolean>(value: T): string {
    if (typeof value ==='string') {
        return `"${value}"`;
    } else if (typeof value === 'number') {
        return value.toString();
    } else {
        return value? 'true' : 'false';
    }
}

let stringResult = stringifyValue('Hello');
let numberResult = stringifyValue(42);
let booleanResult = stringifyValue(true);

stringifyValue 函数中,通过泛型 T 约束为 string | number | boolean 联合类型,我们可以处理不同类型的值,并返回它们的字符串表示。

总结交叉类型和联合类型的注意事项

  1. 类型兼容性:在交叉类型中,同名属性的类型必须兼容。在联合类型中,访问属性和方法时要注意只有所有联合类型共有的才能安全访问,或者通过类型保护来处理。
  2. 代码可读性:合理使用类型别名、交叉类型和联合类型可以提高代码的可读性和可维护性。给复杂类型赋予有意义的名称,使代码更易于理解。
  3. 运行时行为:虽然 TypeScript 提供了强大的类型系统,但类型检查主要在编译时进行。在运行时,要确保通过类型保护等机制正确处理不同类型的值,以避免运行时错误。

通过深入理解交叉类型和联合类型的概念、语法以及应用场景,我们可以更好地利用 TypeScript 的类型系统,编写出更健壮、可维护的代码。无论是处理复杂的对象结构、函数参数,还是与泛型等其他特性结合使用,交叉类型和联合类型都为我们提供了灵活且强大的工具。