Switch with a Functional and Generic Turn
Can we write switch statement in a functional way, while making its type safe and generic? Before we tell the story, let’s look at our…

Can we write switch statement in a functional way, while making its type safe and generic? Before we tell the story, let’s look at our final solution:interface MatchType<X, Y> {
on: (
pred: (x: X) => boolean,
fn: (x: X) => Y
) => MatchType<X, Y>;
otherwise: (fn: (x: X) => Y) => Y;
}
function matched<X>(x: X) {
return {
on: () => matched(x),
otherwise: () => x,
};
}
export function match<X, Y>(x: X): MatchType<X, Y> {
return {
on: (pred: (x: X) => boolean, fn: (x: X) => Y) =>
pred(x) ? matched(fn(x)) : match(x),
otherwise: (fn: (x: X) => Y) => fn(x),
};
}
Now the Story Begins
We are all familiar with javascript’s switch
statement:let color = "grey"
switch(tag) {
case "warning":
color = "red";
break;
case "success":
color = "green";
break;
default:
color = "grey";
}
This statement suffers from a few issues:
- It is not functional, and we don’t like what’s not functional.
- Writing
break
repeatedly seems redundant.
We can easily wrap the code in a functionconst getTagColor = (tag) => {
switch(tag) {
case "warning":
return "red";
case "success":
return "green";
default:
return "grey";
}
};const color = getTagColor(tag);
This way we get rid of the mutable variable and remove the unnecessary use of break
. Still, this is not optimal if:
- One needs to repeatedly write a lot of such code fragment.
- Every time we apply this design pattern, we end up writing the whole function wrapper for our switch logic. Can we avoid this redundant work?
With a Functional Turn
Thanks to this post we have a solution for switch with a functional twist:const matched = x => ({
on: () => matched(x),
otherwise: () => x,
});const match = x => ({
on: (pred, fn) => (pred(x) ? matched(fn(x)) : match(x)),
otherwise: fn => fn(x),
});
Now, we can rewrite our code:const color = match(tag)
.on(x => x === "warning", () => "red")
.on(x => x === "success", () => "green")
.otherwise(() => "grey");
Much nicer! This code takes care of the following concerns:
- Reusable functional switch statement.
- Switch body is only executed when expression is matched. The semantics of the original switch is respected.
- A more complicated condition (a predicate function) can be used instead of only a simple expression.
The problem with this is that in typescript we need everything to have type. We can, of course, take the lazy way to mark everything with an any type:const matched = (x: any) => ({
on: () => matched(x),
otherwise: () => x,
});
const match = (x: any) => ({
on: (
pred: (x: any) => boolean,
fn: (x: any) => any
) => (pred(x) ? matched(fn(x)) : match(x)),
otherwise: (fn: (x: any) => any) => fn(x),
});
The problem is obvious: this function erases all type information, the returned value no longer can keep its proper type. Our IDE is giving us the green light on the following snippet.
- Without the type we can’t know potential typo from compile time
- Any further operation on the matched value would be uncheckable.
With a Generic Turn
This is why we need the generics in typescript — so that our match function can keep type information. The first attempt is intuitive, add 2 type variables, one for the input expression type, one for the matched result type.function matched<X>(x: X) {
return {
on: () => matched(x),
otherwise: () => x,
};
}function match<X, Y>(x: X) {
return {
on: (pred: (x: X) => boolean, fn: (x: X) => Y) =>
pred(x) ? matched(fn(x)) : match(x),
otherwise: (fn: (x: X) => Y) => fn(x),
};
}
Strangely, your IDE (with proper setting) should warn you about the type signature of on
. If you check on the inferred type, you realize this doesn’t behave as expected:

My IDE shows color
as type any
! This is because inferring the type of on
is a recursive function. And typescript is having trouble inferring this recursive type (because on
returns match
, which has a member function on
, then returns match
, … etc.). This is the type inferred for match
:on(
pred: (x: X) => boolean,
fn: (x: X) => Y): {otherwise: (fn: (x: X) => unknown) => unknown, on: (pred: (x: X) => boolean, fn: (x: X) => unknown) => {otherwise: (fn: (x: X) => unknown) => unknown, on: (pred: (x: X) => boolean, fn: (x: X) => unknown) => {otherwise: (fn: (x: ...) => ...) => unknown, on: (pred: (x: ...) => ..., fn: (x: ...) => ...) => ...}}}
With a Typesafe and Generic Turn
The way to solve it would be to give match
a specific type:interface MatchType<X, Y> {
on: (
pred: (x: X) => boolean,
fn: (x: X) => Y
) => MatchType<X, Y>;
otherwise: (fn: (x: X) => Y) => Y;
}
function matched<X>(x: X) {
return {
on: () => matched(x),
otherwise: () => x,
};
}
export function match<X, Y>(x: X): MatchType<X, Y> {
return {
on: (pred: (x: X) => boolean, fn: (x: X) => Y) =>
pred(x) ? matched(fn(x)) : match(x),
otherwise: (fn: (x: X) => Y) => fn(x),
};
}
Specifying the return type of the match function stops the recursive, never-ending type inference process, and finally gives us what we want. The IDE is highlighting all the mistakes for us now!

So, always enjoy a healthy dose of generic type and functional programming.
And with that, you’ve crossed another level to becoming a boss coder. GG! 👏
I hope you found this article instructional and informative. If you have any feedback or queries, please let me know in the comments below. And follow SelectFrom for more tutorials and guides on topics like Big Data, Spark, and data warehousing.
The world’s fastest cloud data warehouse:
When designing analytics experiences which are consumed by customers in production, even the smallest delays in query response times become critical. Learn how to achieve sub-second performance over TBs of data with Firebolt.