Skip to content

Match expressions

Match expressions are sort of like switch statements.

ts
const status: "success" | "failure" | "pending" = getStatus()
match (status) {
    "success" => print("Yay!")
    "failure" => print("Boo!")
    "pending" => print("Waiting…")
}

Assignment

You can assign the result of a match to a variable:

ts
const points = match(grade) {
    "A" => 100
    "B" => 80
    _   => 0
}

Blocks of statements

The match arms can be single expressions or blocks of statements.

ts
const val = match(result) {
    success(v) => {
        print(v)
        return v * 2
    }
    failure(e) => e.message
}

Returns

You can return a value from a match expression:

ts
def classify(result: Result<number>): string {
    return match(result) {
        success(value) => "got value: ${value}"
        failure(error) => "got error: ${error}"
    }
}

Multi-expression blocks must explicitly return a value, whereas single-expression arms return their value implicitly.

Returning an object literal

Just like in JavaScript, if you want to implicitly return an object literal from a single-expression arm, you need to wrap it in parentheses.

ts
kind => ({ label: kind })

Or just use the block form, which doesn't require parentheses:

ts
kind => { return { label: kind } }

Returns inside a match

Inside a match arm, return expr returns from the match, not from the enclosing function:

ts
def classify(result: Result<number>): string {
  match(result) {
    // these return from the arm, not from classify()
    success(v) => return "got a success!"
    failure(e) => return "got a failure!"
  }
}

To return from the function, put the return before the match:

ts
def classify(result: Result<number>): string {
  return match(result) {
    // here you can have a return or not -- single expressions
    // use implicit return.
    success(v) => "got a success!"
    failure(e) => return "got a failure!"
  }
}

goto

You can also use match with goto to jump to different nodes:

ts
node foo() {
  print("hi from foo!")
}

node bar() {
  print("hi from bar!")
}

node main() {
  const val = "foo"
  match(val) {
    "foo" => goto foo()
    "bar" => goto bar()
    _ => print("hi from main!")
  }
}

Destructuring

You can destructure arrays and objects in match arms:

ts
match (event) {
    ["click", x, y] => handleClick(x, y)
    ["scroll", delta] => handleScroll(delta)
    _ => ignore()
}

Guard clauses

You can add a guard clause if (…) to any arm:

ts
match (request) {
    { kind: "user", age } if (age >= 18) => allow()
    { kind: "user" }                     => block()
    _                                    => unknown()
}

The guard runs only when the arm's pattern matched, and it can use any variable that the pattern bound (like age above). If the guard is false, the match tries the next arm.

Catch-all arm

You can use _ as a catch-all arm. It matches anything that didn't match any of the previous arms:

ts
match (value) {
    1 => print("one")
    2 => print("two")
    _ => print("something else")
}

Note: a guard on _ makes it stop being a catch-all. _ if (cond) is a conditional arm, and when cond is false, that arm won't match.

Exhaustiveness checking

One of the really useful things about match expressions is that they do exhaustiveness checking. Suppose you have defined a Shape type and use a match expression to compute the area of different shapes:

ts
type Shape = { kind: "circle", r: number } | { kind: "square", side: number }

const area = match(shape) {
    { kind: "circle", r } => 3.14 * r * r
    { kind: "square", side } => side * side
}

If you add a new shape to your shape type and forget to add it to the match expression, the match expression will throw a type error. This makes it easy to make changes to your code without accidentally forgetting to handle a new case.

Open types always need a _ arm

In the example above, it was easy for us to enumerate all the shapes we can support. But what if the type we're matching on is a string? In that case, it's not possible to enumerate all the types in our match block.

ts
// name is a plain string
const greeting = match(name) {
    "Ada"  => "Hi Ada!"
    "Alan" => "Hi Alan!"
}
// But what if `name` is `Beyonce`?

So whenever the scrutinee (the thing you're matching on) is an open type, add a _ arm:

ts
const greeting = match(name) {
    "Ada"  => "Hi Ada!"
    "Alan" => "Hi Alan!"
    _      => "Hello, you!"
}

Restrictions

You can't use match expressions inside parallel blocks.

References