“Pattern matching” with Typescript done right

Peter Fillo
5 min readMay 5, 2019

--

Yeah, Typescript has not built pattern matching in, yet. However, it is capable to build an elegant type matcher. Let’s see how to make one.

What is pattern matching?

Pattern matching is one of the best features in modern programming languages. A nice definition can be found in Scala documentation:

“Pattern matching tests whether a given value (or sequence of values) has the shape defined by a pattern, and, if it does, binds the variables in the pattern to the corresponding components of the value (or sequence of values).”

Although, full-blown pattern matching is supported only by a few languages like Scala or Rust, partial support or imitating some of its features can be found in almost every language. Its concept gives us power to do things better and nicer.

Visitor with pattern matching

Design patterns in 2019? Yes! They are everywhere, but many people use it without knowing their names. All is about their essentials and an common language they provided, not about their implementations.

Visitor is about separation of data structures and functions operate on them, in such a way that new functionality can be added without modifying its structures. It follows Open/Close principle and Separation of concerns.

Visitor with pattern matching in Kotlin [GIST]

The eyes of a good programmer can see other solutions to problem above. One of them can be implemented with good old Polymorphism.

Solution with polymorphism in Kotlin [GIST]

Why visitor?

Both solutions are functional and have its place for certain situations. They differ in terms of extensibility.

  • Visitor is better for extending functionality (marked red)
  • Polymorphism is better for extending structures
  • Expression problem tries to solve both, extending functionality and structures, without modification of other code. These solutions exist, but they are often non-trivial and require specific language features. So in practice, they are rarely used.

What is going to be changed more likely, structures or functions operate on these structures?

In practice data structures tend to be more stable than operations on them. So changes on functionality are more likely than changes on data structures. It is mainly because data are moving often and functions are not. But of course, it depends.

Moving data is much simpler than moving functionality.

Simple type matcher in Javascript

Oh yeah, Javascript! Here it is, visitor implementation with pattern matching. It is quite nice, concise and functional, but you are never gonna know when and where it is going to fail.

Simple type matcher in Javascript [GIST]

The core of the solution (marked red) is adding “type” as a property to objects and create matcher function. There are multiple problems with this implementation (or maybe with Javascript in general):

  • Typos, you are not allowed to make one :)
  • Not exhaustive checking, you have to check it by yourself
  • Checking input arguments, do it by yourself

These are not separate problems, but symptoms of one, missing type checking.

Simple type matcher in Typescript

Is Typescript the silver bullet for Javascript? I do not think so, but it has great power of types and it is done very well.

What do we need to add types to solution above? Here are the ingredients: Generics, Discriminated Unions, Conditional Types.

Generics

Generics are basic way how to create reusable components.

Discriminated Unions

Unions combine multiple types to one type. Discriminated unions are special case for union type, which contains only types with common discriminant. They enable powerful features like exhaustiveness checking and auto type guards.

class Square {
type = "Square" as const //<-discriminant for class, since TS3.4
}
class Triangle {
type: "Triangle" = "Triangle" //<-discriminant class, before TS3.4
}
interface Circle {
type: "Circle" //<-discriminant for interface
}
type Rectangle = {
type: "Rectangle" //<-discriminant for type
}
type Shape = Square | Circle | Rectangle

Conditional Types

Conditional types add the ability to express non-uniform type mappings. In practice, it is a bit of magic :)

  1. Convert discriminated union to union of its discriminants
type Shape = Square | Circle | Rectangle
type ShapeType = Shape["type"]
// Result:
type ShapeType = "Square" | "Circle" | "Rectangle"

2. Convert discriminated union to map of discriminant to its type

type Shape = Square | Circle | Rectangle
type ShapeMap<U> = {
[K in ShapeType]: U extends {type: K} ? U : never
}

type ShapeTypeMap = ShapeMap<Shape>
// Result:
type ShapeTypeMap = {
Square: Square;
Circle: Circle;
Rectangle: Rectangle;
}

3. Convert map of discriminant to its type to map of discriminant to function with its type as a parameter

type ShapeTypeMap = ShapeMap<Shape>
type Pattern<T> = {
[K in keyof ShapeTypeMap]: (shape: ShapeTypeMap[K]) => T
}
// Result:
type Pattern<T> = {
Square: (shape: Square) => T;
Circle: (shape: Circle) => T;
Rectangle: (shape: Rectangle) => T;
}

Here you are, Typescript implementation of visitor with simple type matcher.

Visitor with pattern matching in Typescript [GIST]

The funny part is type conversion (marked red). With adding types, we gain these benefits:

  • Compiler checks types
  • Compiler checks exhaustively all union options
  • Compiler does auto types guards

Gotchas

There is one gotcha in matcher function, where compiler is unable to resolve correct type. I found a similar problem in this Typescript Issue. It is ugly, but it does not affect the solution at all.

A type checking is worth a thousand tests.

Typescript playground

Codepen playground

Conclusion

The pattern matching is nice, but an idea behind this example is, how to get part of type information from compile-time into run-time within Typescript through union discriminant property.

Thanks for reading, hope you enjoyed it and learned something new. Comments with suggestions or corrections are very welcome.

References

--

--