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})
Point 9 - The weakest link in typing
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.