Five years with TypeScript
For the past five years, I’ve been advocating for the use of TypeScript in the majority of open-source & commercial projects, having an overwhelmingly positive experience from my use of the language in projects I’ve been working on.
Introduction
TypeScript is a programming language built upon its older brother, JavaScript, extending the latter with static type definitions. Introduced in 2012, it is developed & maintained by Microsoft. Ever since it was born, the language has been constantly gaining popularity, scoring as the second most-loved technology in Stack Overflow Developer Survey of 2020, beating even Python, and landing only behind Rust!
For the past five years, I’ve been using the language professionally — in both open-source and commercial projects — with great success. In the following article, I will describe how one can improve the type-safety, expressiveness, productiveness & speed of development, not exclusively in the UI layer of the project.
Build-time type checking
The key feature of TypeScript is static type checking, meaning the code is validated for type correctness before it is “compiled” into a lower-level representation (in most cases, that means translating the source to JavaScript). Starting with the most basic type assertions, like passing expected value type as a function’s argument, through smart type inference, generics, discriminated unions & intersection types, TypeScript introduces a handful of clever constructs like mapped types & operators yet to be implemented by JavaScript engines, but already added to the official ECMAScript specification.
Following TypeScript’s rules results in a much higher level of confidence when it comes to the correctness of the solution. I would actually argue, that starting any new project larger than “hello world” in pure JavaScript is a reckless idea, and introducing static type analysis pays off much quicker than expected.
Reasonable configurability & gradual migration
The language comes with a set of configuration options for the compiler. Initiating a new project brings a handful of the most reasonable settings already turned on. Nowadays, strict mode is on by default, preventing the most dreaded errors, like passing null
where a concrete value is expected or overusing the “escape hatch” — any
type.
At the same time, the default configuration doesn’t require much of the developer’s interest, it is sensible, clear, and is far from bringing “hidden bells and whistles”, like eg. in Haskell (I’m looking at you, Haskell’s language directives).
The other TypeScript’s tagline is gradual adoption. Long story short, it enables a smooth migration path for projects previously developed with JavaScript on a way to greatly improved type safety. The adoption can happen one file at a time — it’s no longer a binary decision of either all or nothing.
Language Server Protocol, editor support & refactoring
The tooling around TypeScript provides a Language Server Protocol (LSP) implementation, meaning that any code editor can gain superpowers like:
- syntax highlighting
- code completion
- error reporting
- refactoring
Changing the shared code pieces used to be a leap of faith in JavaScript. Actually, any act of refactoring without the static code analysis is tedious, risky, and error-prone. With TypeScript and LSP, however, it’s safe & easy — the editor is capable of applying the desired change in all occurrences of eg. the variable being renamed.
Handling optional values
If a billion-dollar mistake caused by the null
was not enough, JavaScript comes with another way of signalling the absence of value — the dreaded undefined
.
TypeScript has been evolving in that matter over the last few years. Nowadays, it enables several ways of dealing with the problem and making the code significantly safer.
First, there’s a --strictNullChecks
compiler flag, prohibiting the assignment of both null
and undefined
to a field expected to hold a concrete value. It is a setting exclusive to the language:
In line 6 of the code gist above, however, one can see an inline union type. The string | null
notation literally means: it is ok to assign a value of either type string or null here.
Second, there are optional values defined on types — another example of a feature exclusive to TypeScript. It’s an absolutely valid scenario for a complex type to define fields, which don’t necessarily need to hold concrete value in every case:
The fieldName?: X
notation literally means: expect this field to either hold a value of type X or beundefined
.
Third, applying operations on possibly absent values. TypeScript added the optional chaining and nullish coalescing operators before those were implemented in pure JavaScript, but almost as soon as it was certain that both will make it to the official ECMA language specification:
The optional chaining allows to safely “navigate” through possibly absent values (without throwing an error), while the nullish coalescing operator, or the ??
, enables providing a failover (default) value in case of stumbling upon a null
(think of it as a good, old ||
operator, but for null
or undefined
values only, instead of all “falsy” ones). Again, keep in mind, that this is an example of a feature eventually available in pure JavaScript, too.
Fourth, the generics in TypeScript allow a more functional approach to the problem, using libraries like true-myth, defining higher-order types capable of representing an absence of a value, plus a huge set of operations. If you know Haskell’sMaybe a
type, Rust’s Option<T>
, Scala’s Option[T]
or Java’s Optional<T>
— then true-myth’s Maybe<T>
should look familiar, too:
These wrapper types have one thing in common: they expose a safe, functional interface of methods for working with possibly absent values. I encourage you to read more about that approach in my other blog post.
Over time, I’ve been gradually applying (and even combining) all those techniques, as neither excludes the other and all of those might be useful, depending on a scenario.
Types notation as a ubiquitous language
Compound types can be defined using either type
or interface
keywords. In general, unless you’re about to create a type alias or a mapped type, the latter is recommended. The interface
notation, in fact, is so plain and simple, that it’s easy to comprehend even by developers who don’t use TypeScript but are familiar with any modern statically typed language.
I’d argue that while JSON is a widely known serialization format, TypeScript’s interface notation is actually very similar and can be used across development teams to define API contracts.
When a new API has to be exposed for the consumers (web apps, mobile apps) and two teams sit together to agree on its shape — be it TypeScript UI programmers & Scala backend developers — prototyping with interface
s might sound like a good idea. In fact, I’ve been using this technique with a subset of TypeScript notation as a “ubiquitous language” many times so far, always with success.
Data Transfer Objects
A technique described in the previous section leads to a definition of Data Transfer Objects. Let’s see however how to deal with more complex cases, eg. consisting of optional values. Building upon the previous example, let’s look at what we can do if there’s an optional value in the payload:
While a new field label
is optional (represented by Option[String]
in Scala), in JSON (an intermediate data transfer format between the API endpoint and a UI application consumer) it will be represented by either a string value or a null
. Let’s write down the contract:
Now, remember the section about dealing with possibly absent values? The idea is to map the bare DTO into a domain object using safe wrapper types around optional values. For that, we need a new interface & mapping function:
All plain values (like title
, value
, isBoxed
) are kept intact, while the label
is represented as Maybe<string>
and “lifted” into this wrapper type via Maybe.of
, capable of dealing with possibly null
values.
I’ve been using this technique in several projects with success so far. The boilerplate is minimal & the pattern is repeatable, so it’s always easy to add new entities or extend the existing ones.
Evolution & looking forward to the future
TypeScript is probably one of, if not the most quickly developed programming languages out there. There’s a new minor version released every month (on average) extending the capabilities & enabling new features, often those proposed by the community. The roadmap looks promising too.
TypeScript is being adopted by more and more projects and has already gained the attention of CTOs and architects. Long gone are the days of treating the UI codebase as a black box with monsters inside that only seasoned JavaScript freaks could tame; the language is making a huge impact on the successful delivery.
It is also natively supported by Deno, a rising alternative to Node, advertised as a secure runtime for JavaScript and TypeScript.
The future is bright (and statically typed)!
Summary
Five years ago, starting a new project using TypeScript was a risky challenge. The technology was still relatively new, the configuration wasn’t easy, the documentation was scarce and hardly any library provided type definitions. Back then, many developers hesitated. I decided to give it a shot, believing that the advantages of using static typing mustn’t be ignored any longer and will eventually make the ecosystem grow exponentially.
After those few years, I’m glad I went that way. That was a great investment in technology and now we’re here — almost every new big project (be it a commercial one, or an open-source library) is developed with static typing in mind.
At SoftwareMill, we value type safety. Using TypeScript along with systems developed in Scala seems a natural & complementary solution. Join us, or let us help you build your project!
Thanks to Agnieszka Wiącek, Małgorzata Orzechowska and Tomasz Krawczyk for a review & spellcheck.