This article expands on Todd Motto's idea of Replacing switch statements with Object literals. A very neat and beautiful alternative to the outdated, clunky and verbose switch
statement. By the end of the article you will be provided with a utility function that I crafted based on Todd's solution that is much more developer friendly, so stick to the end!
If you're not interested in the know-how and just want the utility function, scroll down to the last section (All you need in one place).
So what's wrong with the switch statement?
While the switch statement can be useful in certain situations, many argue that it's not Javscript's best design for what it's good for. It is less flexible, less readable and less maintainable than other constructs.
For example, one of the main criticisms of the switch
statement is its fall-through behavior. If you forget to include a break
statement at the end of a case
, the control will fall through to the next case
, leading to unintended behavior as shown in the example below. This can make the code more error-prone and harder to maintain.
switch (fruit) {
case 'apple':
console.log('Apple selected');
// Missing break statement, falls through to the next case
case 'orange':
console.log('Orange selected');
break;
case 'banana':
console.log('Banana selected');
break;
default:
console.log('Unknown fruit');
}
In this example, if fruit is 'apple'
, both "Apple selected"
and "Orange selected"
will be logged.
The alternative Object Literal lookups
Compared to switch
statements, Object Literals are more flexible and expressive.
here's how we can use them to return string
values only.
const getDate = (unit) => {
var date = {
'year': '2024',
'month': 'January',
'day': '21',
'default': 'Default value'
};
return (date[unit] || date['default']);
}
var month = getDate('month');
console.log(month); // January
Sometimes we need to write more complex code and just returning a string
is not enough. We can take the above code a step further and use functions instead of strings in which we can include more complex code.
const getDate = (unit) => {
var date = {
'year': () => {
// do more complicated stuff here
// just returning a string in this case
return '2024';
},
'month': () => {
return 'January';
},
'day': () => {
return '21';
},
'default': () => {
return 'Default value'
}
};
// we return the Object literal's function invoked
return (date[unit] || date['default'])();
}
var month = getDate('month');
console.log(month); // January
But what if we want a fall through behavior? We can easily implement that with object literals, and it's more readable, declarative and less prone to errors. It also doesn't involve adding or removing break
which is what we're looking for.
const getDayType = (day) => {
const isWeekDay = () => {
return 'Weekday';
}
const isWeekEnd = () => {
return 'Weekend';
}
var days = {
'monday': isWeekDay,
'tuesday': isWeekDay,
'wednesday': isWeekDay,
'thursay': isWeekDay,
'friday': isWeekDay,
'saturday': isWeekEnd,
'sunday': isWeekEnd,
'default': () => {
return 'Default value'
}
};
// we return the Object literal's function invoked
return (days[day] || days['default'])();
}
var dayType = getDayType('sunday');
console.log(dayType); // WeekEnd
Turning what we learned into a utility function
Now that we learned how to use Object Literals instead of switch
, let's build a utility function based on what we learned to simplify our lives even further.
Let's call our function switchCase
. It receives an object with 2 properties: cases
and defaultCase
. Cases is the object literal which will hold our cases, and defaultCase
is... well, the default case.
const switchCase = ({cases, defaultCase}) => {
}
switchCase
is a Higher Order Function that returns a Callback Function. The Callback Function receives the switch expression.
const switchCase = ({cases, defaultCase}) => {
return (expression) => {
}
}
Now all the Callback Function needs to do is return the Object Literal's function invoked.
const switchCase = ({cases, defaultCase}) => {
return (expression) => {
return (cases[expression] || defaultCase)();
}
}
That's it! Now let's see an example of how we can use it.
let date = new Date();
const today = switchCase({
cases: {
year: () => date.getFullYear(),
month: () => date.getMonth() + 1,
day: () => date.getDate()
},
defaultCase: () => date
});
today('year'); // current year
today('month'); // current month
today('day'); // current day
today('century'); // default case - returns the current date Object
For typescript users, we can make use of generics to allow users who are going to call the function later on to specify the type that they want the Object Literal Functions to return.
type SwitchCase<T> = {
cases: {[key: string]: () => T},
defaultCase: () => T
}
const switchCase = <T,>({cases, defaultCase}: SwitchCase<T>) => {
return (expression: string) => {
return (cases[expression] || defaultCase)();
}
}
We can also add generics to the expression instead of assigning it a string type so we get intellisense from Typescript in case we later decide to assign our own literal types.
type SwitchCase<T> = {
cases: {[key: string]: () => T},
defaultCase: () => T
}
const switchCase = <T,>({cases, defaultCase}: SwitchCase<T>) => {
return <S,>(expression: S) => {
return (cases[expression as string] || defaultCase)();
}
}
Ant this is how we use it. Note that we don't always have to specify the type since Typescript automatically infers it unless it's a union of multiple types as shown below. Moreover, adding the literal types in TimeUnit
is optional and we don't have to pass it, although we lose the intellisense from Typescript if we don't.
type TimeUnit = 'year' | 'month' | 'day';
let date = new Date();
const today = switchCase<number | Date>({
cases: {
year: () => date.getFullYear(),
month: () => date.getMonth() + 1,
day: () => date.getDate()
},
defaultCase: () => date
})<TimeUnit>;
today('year'); // current year
today('month'); // current month
today('day'); // current day
today('century'); // default case - returns the current date Object
All you need in one place
Javascript version
Utility Function:
const switchCase = ({cases, defaultCase}) => (expression) => (cases[expression] || defaultCase)();
Usage:
let date = new Date();
const today = switchCase({
cases: {
year: () => date.getFullYear(),
month: () => date.getMonth() + 1,
day: () => date.getDate()
},
defaultCase: () => date
});
today('year'); // current year
today('month'); // current month
today('day'); // current day
today('century'); // default case - returns the current date Object
Tyepscript version
Utility Function:
const switchCase =
<S extends string>({
cases,
defaultCase
}: {
cases: { [Property in S]: () => unknown }
defaultCase?: () => unknown
}) =>
<T>(expression: S | (string & NonNullable<unknown>)): T =>
(cases[expression as S] || defaultCase)() as T
Usage:
type TimeUnit = 'year' | 'month' | 'day';
let date = new Date();
const today = switchCase<number | Date>({
cases: {
year: () => date.getFullYear(),
month: () => date.getMonth() + 1,
day: () => date.getDate()
},
defaultCase: () => date
})<TimeUnit>;
today('year'); // current year
today('month'); // current month
today('day'); // current day
today('century'); // default case - returns the current date Object
Top comments (1)
Tbh. I do not find this very readable. In the end there is nothing wrong with using switch statements, provided the use case makes sense, in JS and imo. they are very readable. A missing break statement seems to be a rather weak argument for all this overhead. After all it is a matter of preference but nothing more.