Close

TypeScript - Discriminated Unions

[Updated: Nov 13, 2018, Created: Nov 12, 2018]

Often a property/variable may take only a finite set of values. For example a variable 'pageOrientation' can take only two values 'landscape' or 'portrait'. But how to enforce only these finite values? Let's see that in steps.

In TypeScript, we call these finite set of values as singleton type if they are declared as a value rather than a type. For example:

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

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

In above example, the singleton type is 'pageOrientation' which is declared in both classes.

Now we can enforce these two values in a function say doPrint(), by using union of above types, type guards and use of type never:

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 compiler is smart enough to see whether all possible if/else paths related to the value 'pageOrientation' are used, if not then compile time error will be thrown:

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

Output

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

If we call a wrong method then:

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

Output

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

Because of compile time singleton value checking, our IDE is also capable to provide code auto-completion correctly and to provide the error indication as seen above.

Above patten where we combined singleton types with union types and type guards is called Discriminated Unions, where the singleton type property is called the discriminant.

Using type aliases

Instead of using Union types directly (as in above example), we can define a type alias so that it can be reused:

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

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

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

//using type alias
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);

Output

34

In above example, we also used switch block instead of if/else.

Using enums

Enum constants can also be used as discriminants:

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

Output

25

Example Project

Dependencies and Technologies Used:

  • TypeScript 3.1.3
TypeScript - Discriminated Unions Select All Download
  • typescript-discriminated-unions
    • discriminated-unions.ts

    See Also