Reputation: 973
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.
Upvotes: 0
Views: 6206
Reputation: 3753
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).
Upvotes: 1
Reputation: 973
After playing around with this for a bit I came up with the following. There is still duplication but at least there is some cross checking that might help guard against errors. The main problem with this is that it is quite verbose and it is easy to forget one combination.
type Pair<A, B> = [A, B]
const pair = <A, B>(a: A, b: B): Pair<A, B> => [a, b]
type ShirtYellow = Pair<Formats.Shirt, Shirt.Yellow>
type ShirtOrange = Pair<Formats.Shirt, Shirt.Orange>
type FruitOrange = Pair<Formats.Fruit, Fruit.Orange>
type FruitLemon = Pair<Formats.Fruit, Fruit.Lemon>
const ShirtYellow: ShirtYellow = pair(Formats.Shirt, Shirt.Yellow)
const ShirtOrange: ShirtOrange = pair(Formats.Shirt, Shirt.Orange)
const FruitOrange: FruitOrange = pair(Formats.Fruit, Fruit.Orange)
const FruitLemon: FruitLemon = pair(Formats.Fruit, Fruit.Lemon)
export type Item = ShirtYellow | ShirtOrange | FruitOrange | FruitLemon
export const Item = { ShirtYellow, ShirtOrange, FruitOrange, FruitLemon };
Here is my second attempts. This time an object based solution.
type AbstractItem<I extends { kind: Formats, type: any }> = I
export type ShirtItem = AbstractItem<{kind: Formats.Shirt, type: Shirt}>
export type FruitItem = AbstractItem<{kind: Formats.Fruit, type: Fruit}>
export type Item = AbstractItem<ShirtItem | FruitItem>
export const isShirt = (i: Item): i is ShirtItem => i.kind === Formats.Shirt
export const isFruit = (i: Item): i is FruitItem => i.kind === Formats.Fruit
export const getShirt = (i: Item): Shirt|null => isShirt(i) ? i.type : null
export const getFruit = (i: Item): Fruit|null => isFruit(i) ? i.type : null
Upvotes: 2