How we use Typescript with React

Simen Andresen
ImersoTechBlog
Published in
7 min readMar 9, 2022

This is a part of a series of posts that is intended to describe how we write our software at Imerso. We use these as internal guidelines when writing software and at the same time, we hope others can make use of it.

Intro

At Imerso, we have a rather large React code base serving our web platform (87 000 lines of code at the time of writing). We started out with writing pure JavaScript, spent a year or two with Flow, before we settled on Typescript, and have never really looked back since.

To summarize the benefits we get with using Typescript (as compared to plain JavaScript):

  • Easy to keep our code consistent.
  • Prevents a whole class of bugs without too much effort.
  • Improves readability (you’re essentially forced to “document” your code).
  • Makes refactoring easier.
  • Much better auto completion.

With this in mind, we’ll go through some different aspects of Typescript, focusing on the parts we actively use in our React code base. This post also aims to give some guidelines on how to use Typescript in a way that makes our code better.

Key Typescript Features

In this section, we will go through some different features of Typescript that we make heavy use of in our code base, and which after some years has proven to be quite valuable when doing refactoring, bug fixing and extensions to our code. We do expect that the reader is familiar with the basics of Typescript, and statically typed languages in general, so this section will only deal with features that is beyond the basics.

Literal union types

Literal union types gives you a nice way to represent the different permitted values a variable can have:

This gives you a very nice way of narrowing down your types to allow only the values you want. Instead of using a more open type such as string, you can enforce your code to only accept the values that make sense. Also, declaring the exact values you permit improves readability.

Discriminating unions

Discriminating unions (also called tagged unions, sum type) is a powerful technique for dealing with data structures that can take multiple different, but fixed types. A value of a discriminating union type can take one of the different types specified in the union, and uses a “tag” to distinguish between them:

Some benefits of using discriminating unions:

  • Convenient to model data as a union of different “cases”.
  • The Typescript compiler can recognize the control flow when checking the "tag” inside e.g.switch and if/else branches and will make sure to check that you access the right fields specified for the case you filter on. For the same reasons, your IDE will provide auto-completion inside the different branches.
  • When used correctly, your code will be more readable, and can often be used to prevent the need for optional fields:

Template literal types

Template literal types is a rather recent feature, that builds upon string literal types, and uses the same syntax as JavaScript’s string templates. They are a very powerful technique to model string patterns, perhaps best illustrated with some examples:

As well as making the code more type safe, template literal types will, when used correctly, improve readability since it makes it easier to communicate the intent of the code.

Generic functions

Generics is possibly the part of Typescript (or any other similar language for that matter) that can be the hardest to read. However, with the right use of generics, the code could actually become more self documenting, less prone to errors and generally give a better developer experience.

You’ve most likely used generic functions before, perhaps without even knowing. Take React’s useState hook for example, which has the following type signature:

One of the nice things about generics is that it can give a lot of type safety without too much work, take for instance the generic useState:

In the above example, the generic parameter is automatically derived from the function parameter 0 , and we don’t really have to worry much about using the wrong type, the compiler will tell us!

Useful applications of Typescript

Having gone through some of the key concepts, we’ll try to illustrate how we apply some of them to get better type safety and easier-to-maintain code.

Generic React functions and literal union types

To illustrate one of the benefits of using generics, let’s first look at an example component MySelect that does not make use of generics:

With the above example, our MySelect component operates only on string types for the value prop and onSelect. However, in our use case, we actually have an even narrower , literal union type ('mesh' | 'pointCloud') and we therefore get a compilation error when trying to call setValue .

This is actually quite a common case in our code base. And it would be really nice if we could take advantage of literal unions when typing our React components:

Doing this is quite straight forward, we just need to apply what we know about generic Typescript functions to our React function:

Great! We now have a component that is even more flexible (it accepts both strings and numbers, as well as literal subtypes, as value types) while at the same time yielding better type safety!

Extract string literal union types from objects

Typescript has quite a few features that makes it easy to keep the code dry. One example we’ll illustrate here is to extract a literal union type from a constant object value.

Let’s say you want to represent a set of possible values, e.g. to represent possible configuration options:

Now, you want to model the union of different options as a type, e.g. to handle the events of the user picking an option. We could of course define a type directly:

However, we’re lazy and don’t want to write and maintain both the literal types and the constant values. Luckily, Typescript has a trick for this kind of stuff:

Non-empty arrays

Specifying that an array is supposed to be non-empty can be very useful to do at the type-system level:

The usefulness of this will of course vary from case to case, but in cases where empty arrays force us to return some sort of fallback (e.g. null) it might be better to handle the fallback in the place you’re actually calling the method, since this might be very context specific.

Imerso Typescript Guidelines

Above, we’ve gone through some different features of Typescript, and some useful applications. This section is intended to add some guidelines on how to use Typescript in a way that brings consistency to our code base. Also, most of the points below are quite general to any typed language and is something we strive to follow in other places of our stack.

Avoid re-using types to represent different things

The main point of this recommendation is to avoid coupling things that should not be coupled, i.e. don’t use the same type for two things that should be able to change separately.

This recommendation is valid for most statically typed languages, but since Typescript is a structurally typed language, it can be even more tempting to re-use types for different things. Sometimes this might happen because the structure of the two entities might be really similar, and even exactly equal at the time you type it.

To illustrate what we’re talking about:

Say we wanna type an HTTP API resource "Scan“ and its POST body payload:

Since the two models share most of their structure, it might be very tempting, out of convenience, to only declare a single type for both of them, something like:

However, this is generally a bad idea, since it couples two models together, making them harder to reason about, and harder to extend them separately .

To illustrate further why coupling like this is bad:

  • It’s harder to know if id is truly optional
  • Let’s say you want to make name optional in the POST body (we have created a new and shiny auto-name-generator that spits out cool, unique names each time!). This change should have NO effect on the Scan resource itself. However, since we were too lazy to write out the two types separately, we now have to make changes that affects both. Ugh:(

Avoid adding type definition if the type can be derived directly

To avoid unnecessary work, e.g. when refactoring, we should avoid adding type definitions when the type can be derived:

There are valid exceptions to this rule though, so use your best judgement. For instance, if you do some heavy, on-the-fly computations where the output should comply with your expectations, you might want to add some type signature:

Use the most restrictive type possible

This ties into previous points about literal unions etc. Whenever you add a type for something, try to narrow it down as far as possible. Not only does this make the code more type safe, it will often make the code easier to understand because restrictive types adds more specificity. Some examples:

  • If your variable can only take a subset of known strings or numbers, use a literal union type, e.g. const selectedResolution: 1 | 5 | 20 = … instead of const selectedResolution: number = ...
  • Be careful of type widening when deriving types from consts. I.e. make sure to add as const like in this (contrived) example:

Consider literal string unions instead of booleans

Take the following example of a component that renders two radio buttons:

In this example, we used a boolean to represent which radio box was selected. However, this has some drawbacks:

  • Cannot use a boolean state if we want to expand the number of selections
  • The state is not very descriptive when we want to represent a selection of two equal choices. In the example above, the state is biased towards mode1 which does not necessarily make sense.
  • When just reading the state itself, we cannot easily understand what !mode1Selected means.

A better approach for cases like this is simply to use a literal string union to represent the different states:

This should improve readability as well as making it easier to extensions to support more than a simple binary state.

Thanks for reading! Hope you found some of this interesting. Let me know what you think in the comments!

--

--

Simen Andresen
ImersoTechBlog

Co-founder, CTO and full-stack developer at Imerso. Working with 3D scanning and quality control solutions for the construction industry.