Search code examples
typescriptfactorynarrowing

Factory narrowing inside switch statement


Is it possible to narrow type output of a factory create method by literal types? I've managed to get some narrowing with if statements and discriminated unions, but this is creational method so I'm not sure whether it's possible.

class Radio {
    type: "RADIO"; // literal type 
    title: string = "A value";
    selected: boolean = false;

    constructor(radio?: Radio) {

    }
}    

class OptionFactory {
    static create({
        type,
        price = 1.0,
        title = "Option",
        selected = false,
    }: {
        price: number;
        title: string;
        selected: boolean;
    }) {
        switch (type) {
            case "RADIO":
                return new Radio({
                    title,
                    selected,
                    // price,
                });
            case "CHECKBOX":
                return new Checkbox({
                    title,
                    selected,
                    // price,
                });
            case "PRICEOPTION":
                return new PriceOption({
                    title,
                    selected,
                    price,
                });
        }
    }
}

let radioButtons = new Array<Radio>();

tags.push(OptionFactory.create({ type: "RADIO" })); //error ts(2345)

console.log(tags);

Typescript Playground


Solution

  • How about this...

    
    function createFrom({ type, price = 1.0, title = "Option", selected = false }: CreationOptions): FactoryReturn {
      return factoryMap[type]({ price, title, selected });
    }
    
    const factoryMap: FactoryMap = {
      "RADIO": ({ title, selected }: Titleable & Selectable) => {
        return new Radio({ title, selected });
      },
      "CHECKBOX": ({ title, selected }: Titleable & Selectable) => {
        return new Checkbox({ title, selected });
      },
      "PRICEOPTION": ({ price, title, selected }: Titleable & Selectable & Priceable) => {
        return new PriceOption({ price, title, selected });
      }
    }
    
    type Typeable = { type: keyof FactoryMap };
    type Priceable = { price?: number };
    type Titleable = { title?: string };
    type Selectable = { selected?: boolean };
    type FactorySelector = Extract<CreationOptions, Typeable>;
    type FactoryReturn = ReturnType<FactoryMap[FactorySelector["type"]]>;
    
    type CreationOptions = Typeable & Priceable & Titleable & Selectable;
    
    type RadioConstructor = (option: Titleable & Selectable) => Radio;
    type CheckboxConstructor = (option: Titleable & Selectable) => Checkbox;
    type PriceOptionConstructor = (option: Titleable & Selectable & Priceable) => PriceOption;
    
    type FactoryMap = {
      "RADIO": RadioConstructor,
      "CHECKBOX": CheckboxConstructor,
      "PRICEOPTION": PriceOptionConstructor
    }
    
    class Radio {
      constructor(option: Titleable & Selectable) {
        console.log('[Radio]', '[constructor]', option);
      }
    }
    
    class Checkbox {
      constructor(option: Titleable & Selectable) {
        console.log('[Checkbox]', '[constructor]', option);
      }
    }
    
    class PriceOption {
      constructor(option: Titleable & Selectable & Priceable) {
        console.log('[PriceOption]', '[constructor]', option);
      }
    }
    
    console.log(createFrom({ type: "RADIO" }));
    console.log(createFrom({ type: "CHECKBOX" }));
    console.log(createFrom({ type: "PRICEOPTION" }));
    

    WYSIWYG => WHAT YOU SHOW IS WHAT YOU GET