Four years of Typescript: a retrospective

Back in December 2017 I wrote about how we were switching from Javascript to Typescript.

Recently in the pub after work, some colleagues and I were discussing a recent painful migration from Javscript to Typescript in an area of the SR Cloud codebase we’d (clearly) not migrated yet. The “is Typescript worth the effort?” question came up triggering this blog post.

The following is a mixture of some colleagues and my opinions.

A recap

Why did we make the shift towards Typescript in the first place?

Specifically: code that is written in a language that permits the declaration of types gives the programmer very useful information about how the program is supposed to work.

Additionally, and perhaps more importantly, we can now get compile-time assurance that our application works with data that is in the correct shape/type, and therefore avoid a whole class of runtime bugs.

What went well?

I think that the overwhelming feeling on the teams I work with is that using Typescript is an improvement over writing vanilla Javascript. Types have been really valuable in understanding the shape of the data being passed between functions and transmitted across network requests.

We have almost entirely removed Javascript from our tech stack, with the exception of really infrequently modified repositories in our codebase, so the migration piece itself wasn’t abandoned half way through or anything like that.

The Javascript ecosystem has, on the whole, also embraced Typescript (in contrast to Flow annotations) so we’ve thankfully been propped up and supported by the wider developer community.

Refactoring is simpler/less risky as the Typescript compiler now points out all the impacted areas of code when the data structures/function signatures change.

The Typescript compiler APIs have been extremely useful for building custom tooling around frontend module import dependencies to decide what tests should be triggered on a production release.

What could have gone better?

A lot of the feedback I’ve had from engineers revolves around “the type error messages are hard to understand”. Having read many Typescript release notes I’m aware the team at Microsoft have improved things over the years but it can take some significant effort to a) understand complex types b) figure out exactly why the compiler is unhappy and finally c) decide how to fix things.

There is no built-in runtime type validation. We’ve had more than one scenario where the Typescript compiler was happy but the types were just plain incorrect for the data that we were processing at runtime. I think the root of this problem has been migrating an existing Javascript codebase to Typescript where it hasn’t been clear what the original data structures were.

Converting large or complex Javascript codebases to Typescript is hard work and requires a thorough understanding of the existing codebase and data structures to get it right. Some “design patterns” we’d use in Javascript simply aren’t easy to represent or work with in Typescript. Probably because they are bad design patterns…

Some types can be quite verbose and difficult to wrap your head around. I think union types are really powerful but they can become a bit of a nightmare to deal with as you try and convince the compiler that some variable really is the type that you think it is. See previous point.

“Complete” type safety is hard to achieve in a developer friendly way… we’ve tried really hard to avoid both explicit and implicit anys in our codebase, and casting too (eg as ImportantData) and also the safe navigation .! operator, to avoid us accidentally forcing the compiler to treat some data as a type that it isn’t. One of the consequences is that often we end up with application logic that just keeps the compiler happy but is logically entirely unecessary. I think the Typescript team have been reducing the common scenarios where this is required over time, so I expect things to improve further.

More specifically the teams I work with have had two large speedbumps in its journey with Typescript. The first was the adoption (and subsequent ongoing removal) of fp-ts which added extra levels of complexity to an already large and complicated codebase with incomplete and not-always-correct types. The motivation for fp-ts adoption was superior type safety heavily inspired by Haskell and friends. For both new and existing members of the team this additional overhead on top of the existing challenges with Typescript made it a lot harder to work on the product. The second was Redux Saga’s use of the yield keyword and Typescript’s inability (fixed in v3.6 apparently) to know what data type had been generated.

Despite good ecosystem support, 3rd party types defined via https://github.com/DefinitelyTyped/DefinitelyTyped don’t necessarily match up to the reality of the libraries they are supposed to provide types for, and versioning consistency between the @types/ modules and the actual modules is a mess.

What next?

even with my limited experience with JS I don’t want even try to go back to that old days, and will take TS over JS every single day. But TS is not perfect, it has it flaw with complex types, that are hard to understand and when defined incorrectly gives a lot of headache and require a “type debugging” session.

I’m not sure… I don’t think anyone I’ve spoken to is keen to drop or move away from Typescript but the list of “what could be better” is a lot longer than I expected!

What do you all think?