Railroad Programming in TypeScript

Wim Jongeneel
ITNEXT
Published in
4 min readFeb 2, 2020

--

Photo by Denis Chick on Unsplash

Many developers will agree that one of the hardest parts of programming is taking care of all potential outcomes. Tasks that sound trivial at first can have a wild variety of cases to consider. Take for example the task of showing the name of an user on a website, based on a first- and last name field. Is there a logged in user? Does the user have a name? How do we format the name? What to do if we one of the fields is empty? A possible implementation could look as follows:

Here we see that we have to check for null and empty strings. And whoever calls this function has to account for both an Error and the possibility of an empty string. This will lead to try-catch blocks and more if-statements. This also leads to another particularity of TypeScript: throwing stuff. You can throw anything you want, not just errors. And then we come to the fact that TypeScript has no pattern matching on catch blocks that allows you to only catch the ‘things’ you care about like you might know from Java or C#. This can lead to complex logic inside the catch-block to figure out what exactly you got.

Those are all valid, even within the typed heavens of TypeScript

When seeing all of this one has to wonder if there is no better way of dealing with unhappy paths and the potential for invalid data. One possible approach from the functional programming world is railroad programming.

Railroad programming

Railroad programming starts of by going back to he basics of (functional) programming. Every function is an arrow with some input on the left and some output of the right. No strings attached, so now throwing of errors. And think about it, is an error not as well a valid output of a function? ‘Error’ is just an output that is less-positive then we wanted, but doesn’t chance the concept of input → output. To make this more practical we can define a type for a function that can fail:

And with those types we can define some constructors and rewrite the renderUser function:

If you now would call renderUser you get full type safety on your errors, forcing you to deal accordingly with them. This has huge advantages when code bases grow in side and errors can bubble up through multiple layers of abstractions before your user sees them on the screen.

Composition

The next step to take is redefining the getUser function to also use our typed error system. A possible way of doing this is shown below.

Here you can note that we have removed the type unsafety of throwing errors around, but are forced to write an if-statement after each function that could return an error. While this is a logical and arguably positive result of adding the safety (no more errors will go unhanded), it can make it awkward to call a bunch of functions in sequence.

But for this we can define common operations in the form of additional functions. The first is map: it applies a function against the a result if there is any. This used to run a function that can fail first and then do something with the result afterwards (if any). Another one is join: it unpacks nested results to a single result. This is a handy utility if something has returned a Result inside a Result. The last one is then: it feeds the potential output of one function that can fail into an other function that can also fail. This will create a pipeline of processes that all can fail, but have no explicit error handling in them. It is very comparable to then on a Promise. Their implementation is as follows:

Those functions can look a bit hard and abstract at first, but they get very practical when you start using them (people custom to function programming will already have recognized that a Result is a monad). Lets look at a few examples of using those functions:

As you can see, all the function have become incredible simple and short. We can just compose functions together, without caring about potential errors in the process. But if we want to extract the result out of the railroad, the compiler will force us to deal with all the potentials errors we have accumulated.

One last thing I will show you is a function that allows you to use fluent style programming when composing functions together, which will look a lot more readable that the endless nesting of arguments that functional TypeScript often ends up in.

Conclusion

In this article we have seen how small types and functions are very powerful when composed together in bigger solutions for one of the lesser parts of TypeScript. If this interests you, checkout the slides of Scott Wlaschin that explain this in much greater detail for F#. All the code samples are also available on Github.

What do you think of how TypeScript / JavaScript has implemented errors and try-cactch? Do you agree that the type unsafety around throw and catch is a pity and could be some much more pleasant to work with? And what is your favorite way of error handling in TypeScript?

--

--

Software Engineer at Mendix (Rotterdam, The Netherlands) • Student MSc Software Engineering • Functional programming enthusiast