Search code examples
typescriptenumscode-duplication

Combining enums in TypeScript


What would be a good way of combining several enums in TypeScript? My first intuition would tell me to do as follows but this creates code duplication which is prone to errors.

export enum Formats {
  Shirt = 'shirt',
  Fruit = 'fruit',
}

export enum Shirt {
  Yellow = 'yellow',
  Orange = 'orange',
}

export enum Fruit {
  Orange = 'orange',
  Lemon = 'lemon',
}

export enum Item {
    ShirtYellow = 'shirt:yellow',
    ShirtOrange = 'shirt:orange',
    FruitOrange = 'fruit:orange',
    FruitLemon = 'fruit:lemon',
}

Use case example. The enums are used to describe four different dialog windows. Shirt dialog handler has defined two dialog windows yellow and orange. The yellow shirt dialog and the orange shirt dialog differ so much that using the same type of dialog for them is not possible. The shirt dialog handler doesn't understand fruit dialogs. The fruit handler is similar but opposite. There is also a global dialog manager responsible for making sure that only one dialog is open at any given time. The global window manager contains a variable representing the open dialog. This variable is stored on the disk to preserve open dialog state over app/page reload.


Solution

  • I think we should not focus on primitive value types like enums. A proper record or class can do what you want. TypeScript allows you to build "discriminated unions", i.e., a family of types that can be distinguished by one field (the "tag"):

    export enum ShirtOptions {
      Yellow = 'yellow',
      Orange = 'orange',
    }
    
    export enum FruitOptions {
      Orange = 'orange',
      Lemon = 'lemon',
    }
    
    interface Shirt {
        kind: 'shirt';
        options: ShirtOptions;
    }
    
    interface Fruit {
        kind: 'fruit';
        options: FruitOptions; // Can have a different name
    }
    
    type Format = Shirt | Fruit;
    
    function handler(f: Format) {
        switch (f.kind) {
            case "shirt": return doShirtStuff();
            case "fruit": return doFruitStuff();
        }
    }
    

    And TypeScript does exhaustiveness checking on the switch statement and will tell you, if you don't handle all the cases (See the link for details).