Reputation: 17180
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
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