Types of TypeScript typings

A brief overview of the different approaches TypeScript offers to make our React code type safe.

Mathieu Anderson
6 min readSep 5, 2019
Picture of an old typewriter
Photo by Patrick Fore on Unsplash

Disclaimer: this article will use TypeScript in the context of React and assumes basic knowledge about React.

Freedom of choice is hard

It is obvious these days that TypeScript is here to stay in the web dev world, especially combined with React. However, many developers have had no experience with strongly typed languages, and the TypeScript documentation can be intimidating.

TypeScript’s documentation example for generics
Hmm yes I see.

One of the reasons for this is that TypeScript (TS for short) offers us many ways to leverage its type system, while not explicitly recommending any specific approach. In this article, we will explore some of these approaches. It will only scratch the surface of what is possible, though!

TLDR; There are many ways to make your code safer with TypeScript. Jump directly to the end of the story or go directly to the CodeSandbox to play around with the code we will write!

In the beginning there was JavaScript

Let’s say we have a JsInput component that is a simple wrapper around an input field. It could look like this :

Vanilla JavaScript input

It is fine. But nothing prevents us from passing some outlandish variable as value, and we will only know at runtime, when we get unexpected results or even a nice crash. That is exactly the problem that TS solves, by offering us type errors at compile time, before you try to run your code. And, even better, directly in our IDE. Let’s see how.

Inline types

The most straightforward approach is to simply annotate types inline, enforcing a check on the props passed to our Input component.

TypeScript input with inline types

Here we say that props will be an object with the keys value and label. value can be either a string or a number, and label will always be a string. We can now know at any point what type we are dealing with :

Gif of hovering over variables to see types
You will get addicted to this

And we are already protected against a whole class of bugs by squiggly red lines in the IDE (and noisy scary errors in the terminal)! We can’t pass an object as value :

TypeScript error : Type ‘{ value: number}’ is not assignable to type ‘string’
Thank you squiggly red lines!

But we also can’t pass any prop that we have not defined and assigned a type to :

TypeScript error: Property ‘another’ does not exist
I’m a fan of the squigglies

This feels very close to writing propTypes for every component, with a more pleasant syntax. And with only this very simple step, we have already made this code orders of magnitude safer. But there is more!

Defining a type

Maybe we don’t want out types to be so tightly coupled with our component. Maybe we use the same Input in many places! Then maybe it is time to define a type we will also be able to reuse anywhere. This is how simple it is :

TypeScript input with defined types

Now, any time we need to pass a value and a label to an Input component, we can use the types we created for that express purpose! For this very simple example, this would be fine in real life. But let’s go deeper.

Defining an interface

An interface defines an entity, representing a contract that must be followed. It can contain any number of properties, and TS uses duck typing to define if we are conforming to the contract or not. If it looks ok, it will be accepted by the compiler. To put it simply, it is like an object, where keys are associated with types instead of values :

TypeScript input typed with an interface

Doesn’t it looks nice ? We have used our previously defined types to define an interface that specify the contract the props passed to InterfaceTsInput must conform to :

  • it must be an object
  • with a value key which must be of type InputValue, that is to say string or number
  • and with a label key which must be of type Label, that is to say string

To be fair, this is also possible with a simple type, like so :

type InputProps = {
value: InputValue;
label: Label;
}

So you might be wondering : “What is the difference between type and interface, then ?”. And you are not alone.

Google search result for “TypeScript dif” autocomplete to “TypeScript difference between type and interface”
When 14 million people asked the same question

The answer is subtle. The main difference is that an interface can be extended (we’ll do it in the next part), while a type cannot. As the more flexible and powerful tool, interface is generally preferred for anything more complex than primitive types.

We are starting to feel safe, aren’t we? But there’s one last thing. Are you ready to get into that weird syntax we saw at the start of the story?

Using generics

Generics are one of the core features of TS. It gives us the possibility to abstract types. This means passing types like we would pass arguments to a function. Ideally, it leads to extremely reusable and composable types. In practice, it is very easy for it to get out of hands and introduce a whole new world of complexity to your app. To be used wisely. In our example, it could look like this :

Here it is! The dreaded T! You can think of it simply as a placeholder, an argument name for the type that we then pass to our GenericInput interface.

That T is then narrowed down when we define GenericTSInput: it extends InputValue, meaning that now T can only be of the types defined by InputValue (string or number). But where do we pass the T, then? How will our component ever know what T is ?

A cup of tea
Photo by Drew Taylor on Unsplash

Before now, nothing had changed in the way we use our Input component. We would use it just as we would a regular React component. This changes when we introduce generics, because we need to specify the type of T. And as you can see in the gist above, this is how we do it, and what it means :

// An input that only accepts a number value
<GenericTSInput<number>
value={1}
label="TS generic type (number)"
/>
// An input that only accepts a string value
<GenericTSInput<string>
value="Value"
label="TS generic type(string)"
/>

By specifying in those brackets that T is a number or a string, we are narrowing down the possible types of InputValue to be only one or the other. And of course, TS doesn’t allow us to break this rule:

TypeScript error for generics
You’re always there for me, squiggly red lines

Freedom of choice is still hard

As we have seen, TypeScript’s type system is a spectrum. It provides you with a number of building blocks for you to use as you see fit. It can be as simple or as complex as you need it to be. Emphasis on need. As ever, and even more in TypeScript’s case, premature optimization will cause some grief ultimately.

Our TypeScript code should always serve the same core goals, and adopt the simplest solution that satisfies them :

  • make your code safer to write with instant feedback on type errors from your IDE or the compiler
  • easier to maintain by ensuring you are not breaking any contracts your types have defined when adding more features
  • more readable for people new to the codebase by giving them some context about your data structures

I encourage you to experiment with the code we have been through in this CodeSandbox so you can get a better feel for what works and what doesn’t, And maybe you can try to add the missing onChange handler to all these inputs to get rid of that error!

--

--