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…

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

  1. It is not functional, and we don’t like what’s not functional.
  2. 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:

  1. One needs to repeatedly write a lot of such code fragment.
  2. 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:

  1. Reusable functional switch statement.
  2. Switch body is only executed when expression is matched. The semantics of the original switch is respected.
  3. 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.

  1. Without the type we can’t know potential typo from compile time
  2. 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.

Read more

在優比快Cloud Team工作是什麼樣子

在優比快Cloud Team工作是什麼樣子

如果你正在找一份可以安安靜靜寫程式、不需要太多溝通的工作,老實說——Ubiquiti Cloud Team 可能不適合你。 年輕的工程師通常在意的是能不能學習、有沒有人帶;而資深工程師,則更看重領域的深度與發揮空間。這兩種我都理解,也都經歷過。在 Ubiquiti Cloud Team,工作確實不輕鬆,問題通常也不單純。但如果你追求挑戰、在意技術如何帶出產品價值,這裡就是個能讓你不斷磨練、逐步放大的舞台。 一些基本資訊先講清楚:我們使用 GitHub,開發環境現代化,雲平台該用的都有;團隊內部提供各種 AI coding 工具輔助日常開發(包括我本人非常依賴的 ChatGPT, Cursor 和 Claude Code);工作型態彈性大,遠端、無限假、健身補助。 一切從「真實世界的裝置」開始 Ubiquiti 跟多數純軟體公司不太一樣,我們的雲端服務是為了支援全球各地數以百萬計的實體網通設備:從 AP、

By schwannden