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

TypeScript类型别名、并集和交集运用

2024-08-233.1k 阅读

TypeScript类型别名

在TypeScript中,类型别名是给一个类型起一个新名字。它主要通过 type 关键字来定义。类型别名不仅可以为基本类型、联合类型、交叉类型等起别名,还能为更复杂的类型,比如函数类型、对象类型等创建别名。

基本类型别名

最基础的应用就是为基本类型创建别名。例如,我们日常开发中经常会使用到数字类型来表示年龄,为了让代码语义更加清晰,可以为 number 类型创建一个别名。

type Age = number;
let myAge: Age = 25;

这里我们通过 type Age = number 定义了一个名为 Age 的类型别名,它代表 number 类型。之后就可以像使用 number 一样使用 Age 来声明变量。

联合类型别名

联合类型表示一个值可以是几种类型之一。通过类型别名,我们能为联合类型创建一个更具描述性的名字。假设我们在开发一个图形绘制的应用,图形可能是圆形或者矩形。圆形可以用半径描述,矩形可以用宽和高描述。

type Circle = { type: 'circle'; radius: number };
type Rectangle = { type:'rectangle'; width: number; height: number };
type Shape = Circle | Rectangle;

function draw(shape: Shape) {
    if (shape.type === 'circle') {
        console.log(`Drawing a circle with radius ${shape.radius}`);
    } else {
        console.log(`Drawing a rectangle with width ${shape.width} and height ${shape.height}`);
    }
}

let circle: Circle = { type: 'circle', radius: 5 };
let rectangle: Rectangle = { type:'rectangle', width: 10, height: 5 };

draw(circle);
draw(rectangle);

在上述代码中,我们首先定义了 CircleRectangle 两种类型,分别表示圆形和矩形。然后通过 type Shape = Circle | Rectangle 创建了一个联合类型别名 Shape,它表示一个值要么是 Circle 类型,要么是 Rectangle 类型。在 draw 函数中,我们根据 shape.type 的值来判断具体的图形类型并进行相应的绘制操作。

函数类型别名

函数类型别名可以让我们更方便地定义和使用函数类型。比如,我们在开发一个数据处理的库,可能有多个函数都遵循相同的函数类型。

type UnaryFunction = (arg: number) => number;

function addOne(num: number): number {
    return num + 1;
}

function multiplyByTwo(num: number): number {
    return num * 2;
}

let operation: UnaryFunction;
operation = addOne;
console.log(operation(5)); 
operation = multiplyByTwo;
console.log(operation(5)); 

这里我们定义了一个函数类型别名 UnaryFunction,它表示接受一个 number 类型参数并返回一个 number 类型值的函数。addOnemultiplyByTwo 函数都符合这个类型。我们可以通过 UnaryFunction 类型别名来声明变量 operation,然后将符合该类型的函数赋值给它,实现灵活的函数调用。

类型别名与接口的区别

虽然类型别名和接口在很多场景下功能相似,但它们之间还是存在一些区别。

  1. 扩展方式
    • 接口:接口可以通过 extends 关键字继承其他接口,实现接口的扩展。例如:
interface Animal {
    name: string;
}
interface Dog extends Animal {
    bark(): void;
}

这里 Dog 接口继承了 Animal 接口,并添加了 bark 方法。

  • 类型别名:类型别名不能使用 extends 关键字进行继承,但可以通过交叉类型来实现类似的功能。例如:
type Animal = { name: string };
type Dog = Animal & { bark(): void };

这里通过交叉类型将 Animal 类型和一个包含 bark 方法的类型组合在一起,实现了和接口继承类似的效果。

  1. 声明合并
    • 接口:接口支持声明合并,即多次声明相同名字的接口,TypeScript 会将它们合并成一个接口。例如:
interface Point {
    x: number;
}
interface Point {
    y: number;
}
let p: Point = { x: 1, y: 2 };

这里两个 Point 接口声明被合并成一个包含 xy 属性的接口。

  • 类型别名:类型别名不支持声明合并。如果重复声明相同名字的类型别名,会报错。例如:
type Point = { x: number };
// 报错:标识符“Point”重复。
type Point = { y: number }; 
  1. 使用场景
    • 接口:更适合用于定义对象的形状,尤其是当需要进行继承和声明合并时。在面向对象编程中,接口常用于定义类要实现的契约。
    • 类型别名:更加灵活,可以为任何类型创建别名,包括基本类型、联合类型、交叉类型等。当需要为复杂类型创建一个简洁的名称,或者使用交叉类型来组合类型时,类型别名是更好的选择。

TypeScript并集

并集类型(Union Types)表示一个值可以是多种类型中的一种。在TypeScript中,通过 | 符号来定义并集类型。

简单并集类型示例

let value: string | number;
value = 'hello';
console.log(value.length); 
value = 10;
console.log(value.toFixed(2)); 

在上述代码中,value 变量被声明为 string | number 类型,这意味着它可以被赋值为字符串类型或者数字类型的值。当 value 为字符串时,我们可以访问其 length 属性;当 value 为数字时,我们可以调用其 toFixed 方法。

函数参数中的并集类型

在函数参数中使用并集类型,可以使函数接受多种类型的参数。例如,我们编写一个函数来打印值,它既可以打印字符串,也可以打印数字。

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

printValue('world');
printValue(42);

printValue 函数中,参数 valuestring | number 并集类型。在函数内部,通过 typeof 操作符来判断 value 的实际类型,然后进行相应的打印操作。

并集类型的类型推断

TypeScript 会根据赋值情况对并集类型进行类型推断。例如:

let result: string | number;
function setResult(value: string | number) {
    result = value;
    if (typeof result ==='string') {
        console.log(result.length); 
    } else {
        console.log(result.toFixed(2)); 
    }
}

setResult('test');
setResult(123);

这里 result 变量是 string | number 并集类型,setResult 函数接受同样的并集类型参数并赋值给 result。在函数内部,根据 typeof result 的结果进行不同的操作,TypeScript 能够正确地进行类型推断。

并集类型与类型保护

类型保护是一种机制,用于在运行时确定一个值的类型。在处理并集类型时,类型保护非常重要。除了 typeof 之外,instanceof 也可以作为类型保护。例如,我们有一个类继承体系:

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

class Dog extends Animal {
    bark() {
        console.log(`${this.name} is barking`);
    }
}

class Cat extends Animal {
    meow() {
        console.log(`${this.name} is meowing`);
    }
}

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

let dog = new Dog('Buddy');
let cat = new Cat('Whiskers');

handleAnimal(dog);
handleAnimal(cat);

handleAnimal 函数中,参数 animalDog | Cat 并集类型。通过 instanceof 类型保护,我们可以在运行时确定 animal 实际是 Dog 还是 Cat,从而调用相应的方法。

TypeScript交集

交集类型(Intersection Types)表示一个值必须同时满足多种类型的要求。在TypeScript中,通过 & 符号来定义交集类型。

简单交集类型示例

type Admin = {
    name: string;
    privileges: string[];
};

type Employee = {
    name: string;
    startDate: Date;
};

type ElevatedEmployee = Admin & Employee;

let newEmployee: ElevatedEmployee = {
    name: 'John Doe',
    privileges: ['admin', 'user'],
    startDate: new Date()
};

在上述代码中,我们定义了 Admin 类型和 Employee 类型,然后通过 type ElevatedEmployee = Admin & Employee 创建了一个交集类型 ElevatedEmployeeElevatedEmployee 类型的值必须同时满足 AdminEmployee 类型的要求,即要有 name 属性、privileges 属性和 startDate 属性。

函数返回值中的交集类型

函数返回值也可以是交集类型。例如,我们编写一个函数,根据不同条件返回包含不同属性的对象。

function getPerson(isAdmin: boolean): { name: string } & ({ privileges: string[] } | { startDate: Date }) {
    if (isAdmin) {
        return { name: 'Admin User', privileges: ['admin', 'user'] };
    } else {
        return { name: 'Regular User', startDate: new Date() };
    }
}

let admin = getPerson(true);
let regularUser = getPerson(false);

console.log(admin.name);
if ('privileges' in admin) {
    console.log(admin.privileges);
}

console.log(regularUser.name);
if ('startDate' in regularUser) {
    console.log(regularUser.startDate);
}

getPerson 函数中,返回值类型是 { name: string } & ({ privileges: string[] } | { startDate: Date })。这是一个复杂的交集和并集组合类型。返回值必须包含 name 属性,同时根据 isAdmin 的值,要么包含 privileges 属性,要么包含 startDate 属性。在使用返回值时,通过 in 操作符进行类型保护,以确保能正确访问相应的属性。

交集类型与接口实现

当一个类实现多个接口时,实际上就相当于创建了一个交集类型。例如:

interface Printable {
    print(): void;
}

interface Serializable {
    serialize(): string;
}

class Document implements Printable & Serializable {
    print() {
        console.log('Printing document');
    }
    serialize() {
        return 'Serialized document';
    }
}

let doc = new Document();
doc.print();
console.log(doc.serialize());

这里 Document 类实现了 PrintableSerializable 接口,这意味着 Document 类的实例同时满足 PrintableSerializable 类型的要求,类似于一个交集类型。

交集类型的应用场景

交集类型在很多场景下都非常有用。比如在开发一个插件系统,可能有不同类型的插件,有些插件需要支持日志记录,有些插件需要支持数据存储。我们可以通过交集类型来定义满足多种功能的插件类型。

type Logger = {
    log(message: string): void;
};

type Storage = {
    save(data: any): void;
    load(): any;
};

type AdvancedPlugin = Logger & Storage;

class MyPlugin implements AdvancedPlugin {
    log(message: string) {
        console.log(`[LOG] ${message}`);
    }
    save(data: any) {
        console.log(`Saving data: ${JSON.stringify(data)}`);
    }
    load() {
        return { loaded: 'data' };
    }
}

let myPlugin = new MyPlugin();
myPlugin.log('Plugin started');
myPlugin.save({ key: 'value' });
console.log(myPlugin.load());

通过交集类型 AdvancedPlugin,我们定义了一个既支持日志记录又支持数据存储的插件类型。MyPlugin 类实现了这个交集类型,从而具备了两种功能。

类型别名、并集和交集的综合运用

在实际项目中,类型别名、并集和交集常常会一起使用,以构建复杂且灵活的类型系统。

示例:图形库的扩展

我们继续以之前的图形绘制应用为例进行扩展。假设我们现在不仅有圆形和矩形,还有三角形,并且有些图形可能是可填充的。

type Circle = { type: 'circle'; radius: number };
type Rectangle = { type:'rectangle'; width: number; height: number };
type Triangle = { type: 'triangle'; side1: number; side2: number; side3: number };

type Fillable = { fillColor: string };

type Shape = Circle | Rectangle | Triangle;
type FillableShape = Shape & Fillable;

function draw(shape: Shape) {
    if (shape.type === 'circle') {
        console.log(`Drawing a circle with radius ${shape.radius}`);
    } else if (shape.type ==='rectangle') {
        console.log(`Drawing a rectangle with width ${shape.width} and height ${shape.height}`);
    } else {
        console.log(`Drawing a triangle with sides ${shape.side1}, ${shape.side2}, ${shape.side3}`);
    }
}

function fill(shape: FillableShape) {
    console.log(`Filling the ${shape.type} with color ${shape.fillColor}`);
}

let circle: Circle = { type: 'circle', radius: 5 };
let fillableCircle: FillableShape = { type: 'circle', radius: 5, fillColor:'red' };

draw(circle);
fill(fillableCircle);

在这个示例中,我们首先定义了 CircleRectangleTriangle 三种基本图形类型。然后定义了 Fillable 类型,表示可填充的属性。通过 type Shape = Circle | Rectangle | Triangle 创建了图形的并集类型 Shape,通过 type FillableShape = Shape & Fillable 创建了可填充图形的交集类型 FillableShapedraw 函数用于绘制普通图形,fill 函数用于填充可填充图形,展示了类型别名、并集和交集的综合运用。

示例:用户权限管理系统

在一个用户权限管理系统中,用户可能有不同的角色,不同角色有不同的权限。

type Admin = { role: 'admin'; permissions: string[] };
type User = { role: 'user'; preferences: string[] };
type Guest = { role: 'guest' };

type Role = Admin | User | Guest;

type CanCreate = { canCreate: boolean };
type CanRead = { canRead: boolean };
type CanUpdate = { canUpdate: boolean };
type CanDelete = { canDelete: boolean };

type AdminPermissions = CanCreate & CanRead & CanUpdate & CanDelete;
type UserPermissions = CanRead;
type GuestPermissions = {};

type UserWithPermissions = (Admin & AdminPermissions) | (User & UserPermissions) | (Guest & GuestPermissions);

function checkPermissions(user: UserWithPermissions) {
    if (user.role === 'admin') {
        console.log(`Admin can create: ${user.canCreate}`);
        console.log(`Admin can read: ${user.canRead}`);
        console.log(`Admin can update: ${user.canUpdate}`);
        console.log(`Admin can delete: ${user.canDelete}`);
    } else if (user.role === 'user') {
        console.log(`User can read: ${user.canRead}`);
    } else {
        console.log('Guest has no special permissions');
    }
}

let adminUser: UserWithPermissions = { role: 'admin', permissions: ['all'], canCreate: true, canRead: true, canUpdate: true, canDelete: true };
let regularUser: UserWithPermissions = { role: 'user', preferences: ['theme-light'], canRead: true };
let guestUser: UserWithPermissions = { role: 'guest' };

checkPermissions(adminUser);
checkPermissions(regularUser);
checkPermissions(guestUser);

在这个示例中,我们首先定义了 AdminUserGuest 三种角色类型,组成了 Role 并集类型。然后定义了不同的权限类型 CanCreateCanReadCanUpdateCanDelete,并通过交集类型定义了不同角色对应的权限,如 AdminPermissionsUserPermissionsGuestPermissions。最后通过并集类型 UserWithPermissions 表示不同角色及其对应的权限。checkPermissions 函数根据用户的角色和权限进行相应的输出,充分展示了类型别名、并集和交集在实际应用中的协同工作。

通过对TypeScript类型别名、并集和交集的深入理解和运用,可以构建出强大、灵活且类型安全的代码结构,提高代码的可维护性和可读性,减少潜在的类型错误。在实际项目开发中,应根据具体需求合理选择和组合这些类型特性,以达到最佳的开发效果。