Orchard Lab

A place to share my thoughts and learnings

Pattern matching in TypeScript using ts-pattern

Posted at — Mar 22, 2022

While I mentioned how great to do pattern match in Elixir. It’s surprisingly good to do this in TypeScript thanks to the ts-pattern.

Here is what I did to grab the content of different RSS feeds using Elixir:

def get_item_content(%{ "content" => %{ "value" => value } }) 
    when is_binary(value), do: value
def get_item_content(%{ "content" => content}) 
    when is_binary(content), do: content
def get_item_content(%{ "description" => %{ "value" => value } }) 
    when is_binary(value), do: value
def get_item_content(%{ "description" => description }) 
    when is_binary(description), do: description
def get_item_content(%{ "content_html" => content_html }), do: content_html
def get_item_content(_), do: "No Content"

And this is the ts-pattern version:

type RSSItemShape =
  | { content: string }
  | { content: { value: string } }
  | { description: { value: string } }
  | { description: string }
  | { content_html: string };
  
function extractContent(item: RSSItemShape): string | null {
  return match(item)
    .with({ content: __.string }, (r) => r.content)
    .with({ content: { value: __.string } }, (res) => res.content.value)
    .with({ description: { value: __.string } }, (res) => res.description.value)
    .with({ description: __.string }, (r) => r.description)
    .with({ content_html: __.string }, (r) => r.content_html)
    .exhaustive();
}

First of all I should say this might looks a little like comparing Apple to Orange. Because Elixir’s pattern matching is a language feature, while the pattern matching of TypeScript is to leveraging the ts-pattern library. The performance of Elixir’s version is most likely better than ts-pattern until someday this was built into V8 node engine, I don’t need to worry the Elixir’s pattern matching won’t work in the future unless syntax changes. But I would need to keep an eye on the version upgrades of ts-pattern which might introduce non-backward compatible changes.

However, today I am just gonna compare it from the developer’s experience perspective, how would it helps us to achieve our goals, express our intention more efficiently.

The first thing I noticed is that both the Elixir version and ts-pattern version are pretty concise. Yes Elixir has more flexibility that you can do the pattern matching on function level or block level. But I think ts-pattern did a great job in terms of readability. Unlike Elixir can absorb this into the language design,  ts-pattern has to be creative within the JavaScript language syntax boundary. But ts-pattern already did a great job by boiling it down to the minimal key words: match, with, otherwise and exhaustive.

Composable. The one lovely thing about ts-pattern is the composable due to its method calls. Composing multiple pattern matching can be quite handy and strait-forward in ts-pattern.

Last thing of course is the type safety ts-pattern can give you. It won’t compile if you are missing cases when using the exhaustive keyword. This could prevent some errors creeping into the runtime. Be careful when passing any object into the match, as ts-pattern has no way to know if you covered all the cases or not. So it will only throw exception during the runtime.

// matching any object
function extractContent(result: any): string | null {
  return match(result)
    .with({ content: __.string }, (r) => r.content)
    .with({ content: { value: __.string } }, (res) => res.content.value)
    .with({ description: { value: __.string } }, (res) => res.description.value)
    .with({ description: __.string }, (r) => r.description)
    .with({ content_html: __.string }, (r) => r.content_html)
    .exhaustive(); // This will not prevent compile time check as we are matching any object
}

Overall I am super happy with ts-pattern, hopefully it can make into JavaScript feature someday, as TypeScript essentially constrained by the target language poses.