Impressions of React and TypeScript from an Elixir/Elm developer
2024-11-16

I was recently on a project where I chose to use React and TypeScript as part of the tech stack because of the existing skillset of my team. I myself managed to avoid both React and TypeScript until this year. So when I finally used them on a real project, I was looking at them as an experienced Elm & Elixir developer, coming from small niche ecosystems into the din of the real world. I think that’s an interesting perspective to share.

React

Due to its huge popularity React has significant upsides – but also very noticeable shortcomings.

On one hand, there is a package available for just about everything. For example, I was able to find a ready-made tool that allowed me to take the OpenAPI spec for backend and generate react-router hooks for use on the frontend. On the other hand, because everyone and their dog is using React, the quality of discussion around it (meaning blog posts, StackOverflow answers and so on) is very low and you have to wade through a lot of noise to get useful answers. In contrast, with Elm and Elixir there’s approximately one forum where all the discussion is happening; you may not find a discussion on a given topic, but if you do, it’s far more likely to involve people who know what they are doing.

Another downside of React’s popularity mixed with it not being a framework is the fact that there are multiple packages for just about anything, including essential tasks, and you are constantly faced with making choices - you need to choose a library for routing, for global state management, for making requests and so on. Often there is no clear winner which makes it more of a headache. In comparison, package ecosystems for more niche languages like Elixir and Elm are much smaller and there is often just one way of doing things (with some exceptions - with Elm, it was like a rite of passage to write your own Dict implementation for some reason).

The hooks model of managing state and effects in recent React versions looked quite appealing initially but it somehow ends up being the uncanny valley of functional programming: it should be good but somehow things are never as simple as they should be. The contortions one has to go through to use setInterval (or resort to a custom hook), the very manual management of dependencies with useEffect, useCallback and the like, the repetitive verbosity of useState – all of it eventually adds up to a tedious experience in comparison to Elixir/Phoenix or Elm.

React has also undergone a lot of architectural changes and accumulated a lot of historical baggage (class-based vs. functional components being the prime example) which complicates things when it comes to working with existing code bases containing a mix of React styles from different years, and similarly when reading documentation or looking for solutions.

My overall impression is that React somehow fails to be an elegant solution: it’s too verbose, it has too many warts, too many ways to shoot yourself in the foot. I guess it came along at the right time and had just the right shape to its learning curve to become enormously popular without being great.

I’d use it again for pragmatic reasons, but I wouldn’t be choosing it because I think it’s an amazing piece of technology. I’d also consider alternatives like Vue and Svelte and htmx if I wasn’t constrained by the capabilities of the team.

TypeScript

I was quite interested in type safety and trying out structural typing but TypeScript was, in many ways, a letdown.

Just like React, it comes with a combination of large upsides and very substantial problems rooted in both its huge popularity, its premise as a superset of JavaScript, and its corporate-backed ethos of being an inclusive space for any feature that could be conceived (Elm creator Evan Czaplicki has a thoughtful talk about this phenomenon: “The Economics of Programming Languages” by Evan Czaplicki (Strange Loop 2023)).

My initial intention for the project was to try using TypeScript both on the frontend and the backend. However, I was in for an unpleasant surprise as I started checking out how to set it up and quickly realised that as of 2024, the backend tooling just wasn’t mature enough to allow this.

Findings from trying to set up TS on the backend:

  • ts-node, supposedly a drop-in replacement, doesn’t appear work properly with Node 20+ESM combo, and using it via the loader might have other problems
  • things like tsx and tsimp and swc run TS code but don’t do type checking (!)
  • Bun also doesn’t typecheck TS (and is like 3 days old). To me, having typechecking as a separate step and not part of the core dev loop defeats the point. Especially when we’re having to deal with the additional complexity of TS including a build step, it better provide rock solid typechecking at least
  • vite-node cannot run Express apps in watch mode
  • There are ways to use Vite but they are hacks.

When I compare this to Elixir, which is kind of similar in that it sits on top of BEAM and Erlang, Elixir is leagues ahead in terms of having things just work without requiring sprawling config or cobbling together a weird assortment of tools.

In the end, I went with a tried and tested approach to implementing the backend in JavaScript where everything is driven by an OpenAPI spec: the Express.js endpoints are generated from it, request and response validation is generated from it, API docs are generated from it, and even frontend client code is generated from the spec as well. This meant that the backend only required a few hundred lines of code and a very small number of tests. The value of TypeScript for a code base like this is minimal, and in my view far outweighed by all the complexity it adds in terms of setup and types.

Even on the frontend, the amount of fiddling and configuration required to work with TypeScript can be quite overwhelming.

Compiler error messages are not too bad but you do get into situations where they are lengthy or obscure, so they aren’t as good as in Elm. I’ve had to resort to type casting in some instances to get things to work.

I think that in practice, many developers struggle to use TypeScript effectively. And when you have a type system that is both complex and optional, guess what’s going to happen? It’s most likely that the use of any, type casting and willy-nilly assortment of ?, ! and | undefined will proliferate, negating most of the value the type system was supposed to bring.

Additionally, the ESLint extension in VS Code proved to be unreliable, periodically losing the plot and producing false positives. I’ve had some problems with Elixir extensions too but it was a lot more unexpected to encounter such problems with a far more widely used tool like ESLint.

TypeScript is, in many ways, an amazing technical achievement, but in some ways that works against it: just because it’s possible to construct a superset of JavaScript in this way, it doesn’t mean that it’s ultimately a great outcome in terms of software quality and developer productivity. People get lured in by static types, perhaps without realising how much complexity they’re buying into in terms of setup and config, and how much expertise is needed in order to take advantage of the tools provided by the language.

Conclusion

I was expecting great things from React and TypeScript based on their popularity but was surprised by how unpolished the developer experience is with both of them, despite massive corporate backing. For me, the developer experience provided by Elixir, Phoenix and LiveView was superior. It seems that small communities like Elm’s can really punch above their weight.

Sure, the ecosystem is far smaller there, and the barrier to entry might be higher in some ways, but if that aligns with your requirements and capabilities, you can have a much better time.

Similarly with Elm, if your requirements don’t extend beyond what Elm and its packages can provide, then the simplicity of the language and the type system can be a delightful experience.

With React and TypeScript, it’s like being in the middle of a megapolis: there are many, many things that are possible, but it’s noisy and overwhelming.

With Elixir or Elm, it’s like being in a small town: there aren’t that many facilities and options, but if they happen to match your needs, you’re going to be very comfortable, and the community can be great too.

Would you like to dive further into Elm?
📢 My book
Practical Elm
skips the basics and gets straight into the nuts-and-bolts of building non-trivial apps.
🛠 Things like building out the UI, communicating with servers, parsing JSON, structuring the application as it grows, testing, and so on.
Practical Elm