Bootstrap

TypeScript中的单例类型与区分联合

在编程中,我们经常会遇到一些变量或属性只能取有限的几个值的情况。例如,一个页面的方向只能是“横向”或“纵向”。那么,如何在TypeScript中限制变量只能取这些有限的值呢?这就需要用到单例类型和区分联合的概念。

单例类型与区分联合

在TypeScript中,单例类型是指那些只包含一个值的类型。例如,'landscape''portrait'都是单例类型。当我们把多个单例类型组合在一起时,就形成了区分联合。区分联合是一种类型模式,它通过一个共同的属性(称为区分符)来区分不同的类型。

示例1:打印机页面方向

我们先来看一个简单的例子,假设我们有两个打印机类,一个用于打印横向页面,另一个用于打印纵向页面:

class PrinterA {
    pageOrientation: 'landscape';
    printLandscape(): void {
        console.log("printing in landscape");
    }
}

class PrinterB {
    pageOrientation: 'portrait';
    printPortrait(): void {
        console.log("printing in portrait");
    }
}

在上面的例子中,pageOrientation就是单例类型。我们可以通过一个函数来根据页面方向调用不同的打印方法:

function print(pt: PrinterA | PrinterB): void {
    if(pt.pageOrientation === 'landscape'){
        pt.printLandscape();
    }else if(pt.pageOrientation === 'portrait'){
        pt.printPortrait();
    }else{
        let unknownPrinter: never = pt;
    }
}

如果我们在函数中遗漏了某个页面方向的处理,TypeScript编译器会报错。例如,如果我们删除了处理'portrait'else if分支,编译器会报错:

build/discriminated-unions2.ts(24,13): error TS2322: Type 'PrinterB' is not assignable to type 'never'.

如果调用了错误的方法,编译器也会报错:

function doPrint(pt: PrinterA | PrinterB): void {
    if (pt.pageOrientation === 'landscape') {
        pt.printLandscape();
    } else if (pt.pageOrientation === 'portrait') {
        pt.printLandscape(); // 错误的方法
    } else {
        let unknownPrinter: never = pt;
    }
}

编译器会报错:

build/discriminated-unions3.ts(21,12): error TS2339: Property 'printLandscape' does not exist on type 'PrinterB'.

由于TypeScript在编译时会对单例值进行检查,我们的IDE也能够正确地提供代码自动补全,并且在出现错误时给出提示。

使用类型别名

除了直接使用联合类型,我们还可以定义类型别名以便复用。例如,我们定义了三种员工类型:

interface FullTimeEmployee {
    empType: "FullType";
    name: string;
    annualSalary: number;
}

interface PartTimeEmployee {
    empType: "PartTime";
    name: string;
    monthlySalary: number;
}

interface ContractEmployee {
    empType: "Contractor";
    name: string;
    hourlySalary: number;
}

// 使用类型别名
type Employee = FullTimeEmployee | PartTimeEmployee | ContractEmployee;

function getEmployeeSalary(emp: Employee): number {
    switch (emp.empType) {
        case "FullType":
            return emp.annualSalary;
        case "PartTime":
            return emp.monthlySalary;
        case "Contractor":
            return emp.hourlySalary;
        default:
            let temp: never = emp;
            return temp;
    }
}

let contractor: ContractEmployee = {empType: "Contractor", name: "Tina", hourlySalary: 34};
let sal = getEmployeeSalary(contractor);
console.log(sal); // 输出:34

在这个例子中,我们使用了switch语句来代替if/else语句,效果是一样的。

使用枚举

枚举常量也可以作为区分符。例如,我们定义了两种形状类型:

enum ShapeType {TRIANGLE, RECTANGLE}

interface RightAngledTriangle {
    shapeType: ShapeType.TRIANGLE;
    base: number;
    height: number;
    hypotenuse: number;
}

interface Square {
    shapeType: ShapeType.RECTANGLE;
    length: number;
    width: number;
}

type Shape = RightAngledTriangle | Square;

function getArea(shape: Shape): number {
    switch (shape.shapeType) {
        case ShapeType.TRIANGLE:
            return (shape.base * shape.height) / 2;
        case ShapeType.RECTANGLE:
            return shape.length * shape.width;
        default:
            let temp: never = shape;
            return temp;
    }
}

let shape: Square = {shapeType: ShapeType.RECTANGLE, length: 5, width: 5};
let area = getArea(shape);
console.log(area); // 输出:25

在这个例子中,shapeType就是区分符,我们通过它来区分不同的形状类型,并计算相应的面积。

总结

通过单例类型和区分联合,我们可以方便地限制变量只能取有限的值,并且能够在编译时检查代码的正确性。这不仅提高了代码的可维护性,还减少了运行时错误的可能性。

;