Sergiy Seletskyy
Sergiy Seletskyy

Reputation: 17180

How to avoid if-else multiple checks (spaghetti code) in typescript

General question: How to avoid if-else ugly code when you have to deal with multiple checks based on some string type?

Example: I have an object "Task" in TODO app, which can be of different types, e.g. simple checkbox, textarea, date, a combination of fields etc. For each type of a "Task", there is different logic on how to change the object before saving it.

How to leverage typescript and organize the code without if-else statements.

Upvotes: 0

Views: 465

Answers (1)

Sergiy Seletskyy
Sergiy Seletskyy

Reputation: 17180

Here is my solution to the problem.

Let's define some basic typescript interfaces.

interface TaskType {
  title: string;
  type: TypeEnum;
  metadata: Record<string, any>; // it stores different kind of objects based on the type
}

enum TypeEnum {
  CHECKBOX = 'CHECKBOX',
  TEXTAREA = 'TEXTAREA',
  DATE = 'DATE',
  WHATEVER = 'WHATEVER',
}

Let's define an interface for the function which is responsible for updating the task based on some form data

// this is what you expect to get from HTML form
export interface TaskFormType {
  metadata: Record<string, any>; // you can define it more strictly according to your needs
}

// interface for function arguments
export interface SaveTaskFnArgs {
  task: TaskType;
  formData: TaskFormType;
}

// interface for the function which should prepare a new version of the task
export interface SaveTaskFn {
  (args: SaveTaskFnArgs): TaskType;
}

Now we can define dedicated SaveTaskFn function for each type of the Task

const checkboxSave: SaveTaskFn = ({ task, formData }) => {
  const updatedTask: TaskType = ... // do what you need to update the task
  return updatedTask;
};
const textareaSave: SaveTaskFn = ({ task, formData }) => { ... }
const dateSave: SaveTaskFn = (args) => { ... }
const whateverSave: SaveTaskFn = (args) => { ... }

Now it's the fun part how actually avoid if-else statements. 1. We define a mapping between task type and a function 2. We define a public saveTaskFn function ( so we can export only this function actually)

// 1
const SAVE_TASK_MAPPING: Record<TypeEnum, SaveTaskFn> = {
  [TypeEnum.CHECKBOX]: checkboxSave,
  [TypeEnum.TEXTAREA]: textareaSave,
  [TypeEnum.DATE]: dateSave,
  [TypeEnum.WHATEVER]: whateverSave,
};

// 2

export const saveTask = (args: SaveTaskFnArgs): TaskType => {
  // all if-else statements collapsed in two lines of code (literally);
  const fn: SaveTaskFn = SAVE_TASK_MAPPING[args.task.template.type]; 
  return (fn && fn(args)) || args.task; // here instead of returning original task ( || args.task) you can raise exception or return some default object based on your architecture
};

Here is an extra benefit of using such mapping. If you extend Task's TypeEnum with a new value, e.g. 'FILE' you will get a typescript error

Error:(<line>, <column>) TS2741: 
Property 'FILE' is missing in type '{ 
  [TypeEnum.CHECKBOX]: SaveTaskFn; 
  [TypeEnum.TEXTAREA]: SaveTaskFn; 
  [TypeEnum.DATE]: SaveTaskFn; 
  [TypeEnum.WHATEVER]: SaveTaskFn; 
  [TypeEnum.FILE]: SaveTaskFn; 
}' but required in type 'Record<TypeEnum, SaveTaskFn>'.

... so you have a reminder to implement a new function e.g. fileSave() and add it to mapping like that

const SAVE_TASK_MAPPING: Record<TypeEnum, SaveTaskFn> = {
   ...
  [TypeEnum.FILE]: fileSave,
};

Upvotes: 2

Related Questions