Switch statement with reduced scope for factory

How can I narrow the type output of a factory create method using literal types? I've tried narrowing with if statements and discriminated unions, but since this is a creational method, I'm not sure if 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

Answer №1

How about this...


function generateFromOptions({ kind, cost = 1.0, label = "Selection", chosen = false }: GenerateOptions): ResultFactory {
  return mapping[kind]({ cost, label, chosen });
}

const mapping: MappingType = {
  "RADIO": ({ label, chosen }: Labelable & Chooseable) => {
    return new Radio({ label, chosen });
  },
  "CHECKBOX": ({ label, chosen }: Labelable & Chooseable) => {
    return new Checkbox({ label, chosen });
  },
  "PRICEOPTION": ({ cost, label, chosen }: Labelable & Chooseable & Costable) => {
    return new PriceOption({ cost, label, chosen });
  }
}

type KindType = { kind: keyof MappingType };
type Costable = { cost?: number };
type Labelable = { label?: string };
type Chooseable = { chosen?: boolean };
type SelectionPicker = Extract<GenerateOptions, KindType>;
type ResultFactory = ReturnType<Mapping[SelectionPicker["kind"]]>;

type GenerateOptions = KindType & Costable & Labelable & Chooseable;

type RadioBuilder = (choice: Labelable & Chooseable) => Radio;
type CheckboxBuilder = (choice: Labelable & Chooseable) => Checkbox;
type PriceOptionBuilder = (choice: Labelable & Chooseable & Costable) => PriceOption;

type MappingType = {
  "RADIO": RadioBuilder,
  "CHECKBOX": CheckboxBuilder,
  "PRICEOPTION": PriceOptionBuilder
}

class Radio {
  constructor(choice: Labelable & Chooseable) {
    console.log('[Radio]', '[constructor]', choice);
  }
}

class Checkbox {
  constructor(choice: Labelable & Chooseable) {
    console.log('[Checkbox]', '[constructor]', choice);
  }
}

class PriceOption {
  constructor(choice: Labelable & Chooseable & Costable) {
    console.log('[PriceOption]', '[constructor]', choice);
  }
}

console.log(generateFromOptions({ kind: "RADIO" }));
console.log(generateFromOptions({ kind: "CHECKBOX" }));
console.log(generateFromOptions({ kind: "PRICEOPTION" }));

WYSIWYG => WHAT YOU SHOW IS WHAT YOU GET

Answer №2

Check out this new approach that restructures your classes and introduces a factory function that deduces the class instance type from the provided "type" property in the initialization:

TS Playground

type BaseOptionInit = {
  selected: boolean;
  title: string;
};

class BaseOption<Type extends string> {
  selected: boolean;
  title: string;
  readonly type: Type;

  constructor (init: BaseOptionInit & { type: Type; }) {
    this.selected = init.selected;
    this.title = init.title;
    this.type = init.type;
  }
}

class Radio extends BaseOption<'RADIO'> {
  constructor (init: BaseOptionInit) {
    super({...init, type: 'RADIO'});
  }
}

class Checkbox extends BaseOption<'CHECKBOX'> {
  constructor (init: BaseOptionInit) {
    super({...init, type: 'CHECKBOX'});
  }
}

class PriceOption extends BaseOption<'PRICEOPTION'> {
  price: number;
  constructor (init: BaseOptionInit & { price: number; }) {
    const {price, ...rest} = init;
    super({...rest, type: 'PRICEOPTION'});
    this.price = price;
  }
}

type OptionType = 'CHECKBOX' | 'PRICEOPTION' | 'RADIO';

type OptionFactoryInit<T extends OptionType> = {
  price?: number;
  selected?: boolean;
  title?: string;
  type: T;
}

type OptionInstanceFromTypeName<T extends OptionType> = (
  T extends 'CHECKBOX' ? Checkbox
  : T extends 'PRICEOPTION' ? PriceOption
  : T extends 'RADIO' ? Radio
  : never
);

function createOption <T extends OptionType>(init: OptionFactoryInit<T>): OptionInstanceFromTypeName<T> {
  const {price = 1, title = 'Option', selected = false} = init;
  switch (init.type) {
    case 'CHECKBOX': return new Checkbox({selected, title}) as any;
    case 'PRICEOPTION': return new PriceOption({price, selected, title}) as any;
    case 'RADIO': return new Radio({title, selected}) as any;
    default: throw new Error('Invalid type');
  }
}


// Example usage:

const priceOption = createOption({type: 'PRICEOPTION'});
priceOption.type // 'PRICEOPTION'
priceOption.price // number

const radios: Radio[] = [];
const radio = createOption({type: 'RADIO'});
radios.push(radio);

radio.type // 'RADIO'
radio.selected // string
radio.title // string

radio.price /* Expected error 👍
      ~~~~~
Property 'price' does not exist on type 'Radio'.(2339) */

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Can the dimensions of a dialog be customized in Angular Material Design for Angular 5?

I am currently developing a login feature for an Angular 5 application. As part of this, I have implemented an Angular Material Design popup. Within the dialog screen, I have a specific process in place: The system checks the user's email to determi ...

Methods for verifying an empty array element in TypeScript

How can I determine if an element in an array is empty? Currently, it returns false, but I need to know if the element is blank. The array element may contain spaces. Code let TestNumber= 'DATA- - -' let arrStr =this.TestNumber.split(/[-]/) ...

Resolving TS2304 error using Webpack 2 and Angular 2

I have been closely following the angular documentation regarding webpack 2 integration with angular 2. My code can be found on GitHub here, and it is configured using the webpack.dev.js setup. When attempting to run the development build using npm start ...

Tips for successfully typing the backtick character when transitioning to Typescript:

I am currently working on a Typescript Vue project involving Leaflet. I came across some code for lazy-loading map markers, but it was written in Javascript. Although the code works fine, I keep receiving errors and warnings from VSCode because this is not ...

In Angular 5, when you reset a required form control in a reactive form, the required error message beneath the input field is not cleared

Within my template, there is a form that becomes visible when a button is clicked- <form [formGroup]="person" (ngSubmit)="onSubmitNewPerson()" #formDirective="ngForm"> <mat-form-field> < ...

A special term in Typescript that consistently points to the present object within the class

Is it feasible to utilize a reference to the current instance of a class in Typescript, similar to "this" in C# and Java, rather than the typical binding context interpretation in JavaScript? ...

Service function in Angular 2 is returning an undefined value

There are two services in my project, namely AuthService and AuthRedirectService. The AuthService utilizes the Http service to fetch simple data {"status": 4} from the server and then returns the status number by calling response.json().status. On the ot ...

Expressjs - Error: Headers already sent to the client and cannot be set

After testing various solutions from others, I am still unable to resolve this error. My objective is to iterate through each item in the array sourced below: novel.ts export let indexList = (req: Request, res: Response) => { novel.getAllDocuments ...

Error encountered while utilizing Array.find with a specific data type

I am trying to locate the user's browser language from a list of supported languages. Here is the code snippet I am using: const userLanguage = browserLanguages.find(language => !!supported[language]); But unfortunately, I'm encountering thi ...

Why does Rollup insist on treating my dependency's TypeScript code as if it were written in JavaScript?

I've included an example code snippet here: https://github.com/thejohnfreeman/bugs/commit/b4ff15a670691ada024589693d22f4fd0abae08d The module called parent is primarily composed of type declarations written in TypeScript. The source entrypoint for Ty ...

What is the best way to include bootstrap using webpack?

I am currently building a webapp using Typescript and webpack. I have been able to successfully import some modules by including them in my webpack.config.js file as shown below. However, no matter how many times I attempt it, I cannot seem to import the b ...

Running two different wdio.config.js files consecutively

Is it possible to run two wdio.config.js files with different configurations, one after another? Here is how the first configuration file is defined in the code: const { join } = require('path'); require('@babel/register') exports.co ...

Utilize switchMap to sequence calls

My goal is to execute rest requests sequentially using switchMap(...) from RxJs. Here is the object: export class Transaction { constructor( public id: string, public unique_id: string, public name: string, public status: string, pu ...

What is the reason for the function to return 'undefined' when the variable already holds the accurate result?

I have created a function that aims to calculate the digital root of a given number. Despite my efforts, I am encountering an issue where this function consistently returns undefined, even though the variable does hold the correct result. Can you help me ...

Issue arose while attempting to use Jest on a React Native application integrated with TypeScript (Jest has come across an unforeseen token)

Seems like everyone and their grandmother is facing a similar issue. I've tried everything suggested on Stack Overflow and GitHub, but nothing seems to work. It should be a simple fix considering my project is basic and new. Yet, I can't seem to ...

What is preventing TypeScript from resolving assignment in object destructuring?

Consider the code snippet below: interface Foo { a?: number b?: number } function foo(options?: Foo) { const { a, // <-- error here b = a } = (options ?? {}) return [a, b] } Why does this code result in the followi ...

Issue with package: Unable to locate the module specified as './artifacts/index.win32-ia32-msvc.node'

I am encountering an issue while using Parcel for the first time. When I execute npx parcel .\app\index.html, I receive the following error: Error: Module not found './artifacts/index.win32-ia32-msvc.node' Require stack: - C:\Users ...

Is MongoDB still displaying results when the filter is set to false?

I am currently trying to retrieve data using specific filters. The condition is that if the timestamp falls between 08:00:00 and 16:00:00 for a particular date, it should return results. The filter for $gte than 16:00:00 is working correctly, but the $lte ...

After using apt to install tsc, I find myself in a dilemma on how to either delete or upgrade it

After realizing I was missing Typescript on my server, I attempted to run the 'tsc' command. However, I received a message suggesting I use 'apt install tsc' instead. Without much thought, I executed the command. Normally, I would insta ...

Unable to retrieve the reflective metadata of the current class instance

Is it possible to retrieve the reflect-metadata from an instance of a class? The documentation provides examples that suggest it should be achievable, but when I attempt to do so, I receive undefined as a result. Strangely enough, when I request the metada ...