Types of TypeScript typings
A brief overview of the different approaches TypeScript offers to make our React code type safe.
--
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.
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 :
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.
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 :
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
:
But we also can’t pass any prop that we have not defined and assigned a type to :
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 :
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 :
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 typeInputValue
, that is to saystring
ornumber
- and with a
label
key which must be of typeLabel
, that is to saystring
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.
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 ?
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:
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!