Typescript and Reasonml Notes

2020-12-22

These notes are something I’ve taken while evaluating reaosnML as a potential candidate to introduce in one of the projects I’ve had worked on. So before rewriting everything I’ve tried to implement more realistic app than regular todo. Two main things which I needed to evaluate were how it deals with async and how easy it is to integrate with the existing codebase. I’ve spent way more time on typescript in general so I might just not have the right knowledge to achieve the same things in reason. But I’ve still put a few hours to figure out how easy it is to get productive with the language. One of the things which I’m evaluating is how easy it is to get productive with the language.

While there been some changes in reason, rebranding it to rescript and making syntax closer to JS, it doesn’t change most of these notes.

Point 1 - Encoding/decoding

To keep the type safety JSON needs to be encoded and decoded to consume or send it. This quickly can get out of hand and become quite verbose. Especially if you are working with complex apps. The package I’ve used was bs-json. Thing is, it’s written in Ocaml, to understand what it does I ended up using reasons REPL which does the translation. Reading code in a new language is hard, having to translate it from similar language is even harder.

Point 2 - Friendly, but not so friendly errors

It’s trying to sell itself as having friendly error messages, sometimes if an error is related to ; it’s not so friendly. Maybe just needs practice.

Point 3 - Multiple pipeline operators

At the time of doing that evaluation reason had (and still has), multiple pipeline operators. Some code uses ->, others will have |>. They are trying to deprecate the |> but even now whilst googling for example you will have to translate it.

Point 4 - Promise Errors

This will pop up often. People claiming that you will have very few runtime errors. But the thing is, with real web app you will need to have some async. If you will use Promises they work very similar to promises in js. You will have then and catch and if you don’t catch, you will get the same big fat error you’d get in JS land.

Point 5 - NO SOURCE MAPS

Debugging code with source maps is not perfect, but it’s definitely better then debugging some compiler output. While you might get by in development where code is still somehow readable, you will need to do the translation in your mind on how reason is translated to js. What is worse is in production, if you are using anything to monitor your front end application with some RUM tool likes entry or similar it’s nice to be able to map the errors to the actual code. With reason, you can forget about that.

Point 6 - Binding to existing libraries

You will see things like [@bs.val] [@bs.new][@bs.module] for cases where you are using existing libraries. In typescript to bind to the 3rd party libraries you just use same type syntax you’d use in your regular typescript. Here you need to add some extra flags. Another caveat is that you might end up with wrong types and it will still work, just giving unexpected results. Also, you’ll see multiple versions of this syntax placing these flags all other the place. Older OCaml syntax was using these at the end of the value, newer one serves as decorators at the beginning.

And similar to typescript, typing existing js library is hard. So you might see things like send2 send3 which are just different versions of the same function. Just accepting different arguments.

Point 7 - Data Structure mismatches

ReasonML has Arrays, Lists, Records and now also Objects. While they might look like directly mapping to Array and Object in js that’s not the case. So when binding to existing libraries this can lead to weird results. It might even work for a bit. Newer versions seem to address that, but I’ve already been bitten by the fact that lists were translated to a linked list made of arrays, and now they were translated to nested objects, making it even more confusing. What now is called record and translates to object used to be different data structure as well. So when binding and now knowing about it you might call List.map on what supposed to have been array and have some unexpected results. Another example that empty List is translated to 0. Works, but when debugging might be awkward.

Point 8 - No Emoji support

Ok, a weird one, but to put in emoji you can’t just put it as a string. If you put it in you will get odd results. To work around this you need to drop in raw strings like ({js|✅|js})

In Typescript, you get away with any, at first reason looks like strongl y typed language, but soon you realize that you can just wrap stuff with raw and still be in no-man lands.

Similar to that, while in reason you don’t have generics 'a types are close enough, and if you bind then incorrectly you are doomed for some weird behaviour.

Point 10 - callbacks

There are a lot of callback based apis in js world. Usually, callbacks are not curried for js. To mimick that you need to use uncurried functions like (. a) => unit when actually passing in the instance of that function we need to use (. ) notation as well.

Point 11 - Single Output format

With typescript, you can choose which version of js you want to output. Meaning you can write ES2020 and just strip types or choose to output code supporting es3 browsers. With ReasonML on other hand, the only option you get is module format. With <90% of browsers supporting modules, meaning they also support modern js, you still end up shipping all sort of stuff to mimick the expected behaviour like spreads and rest parameters.

Point 12 - Type Inference

Ok, here Reason wins big time. In typescript it kind of works, but after a few weeks or months, you just want to yell on TS compiler, because it’s obvious what the type is and reason would pick it up. But in TS, nope, you’ll get it only in very straightforward cases.

Point 13 - Exhaustive checks

This one will go to the Reason as well. But you can create a similar pattern it Typescript. So it’s not impossible, just that you need to do it yourself. Similar to type inference, it works in a special case if you arrange your code in a certain way.

Point 14 - Compile targets

So, with Typescript, you can specify to which language version you want it to compile down. Meaning you can have esnext code written by you and just types stripped out. The output ends up super readable. With things like differential loading, you can work with modules first and ship modern js. With Reason, there is only es5 target. That’s it. This also complicates the reading of compiled code a bit.

Point 15 - It looks like js, but well, it’s not

Typescript’s a superset of javascript. ReasonML/Rescript just looks like it. I liked how Elm creator Evan Czaplicki showed how virtual DOM implementations between Elm and react are different. One of the comparisons was how birds and insects both have wings, yet they are so different in how they work. Similar to this is how ReasonML looks like js, but well, it doesn’t work that way.

Conclusion

Nothing comes for free and without tradeoffs. While you can write runtime bug-free ReasonML code, you can also introduce bugs with escape hatches. Anyone who ever built an app will know, that this will happen, and you will need to ship things ASAP. So it’s up to you and the team to decide what are your priorities and strong suits. Do you have a more senior team with long term focus? You probably can pick up reason. You will get excellent tools, few runtime bugs and super-fast builds. But you’ll need to think through how you will ship it, how to monitor it and how to interact with existing things. Do you have folk coming from javascript and bunch of different requirements? Well, you might like typescript more, it’s flexible and a bit easier to pick up from js. Also, has a wider ecosystem. Again, get ready for slower builds and yelling at compiler why it’s not smart enough to figure out some of the things.

Tags