Disclaimer: This article will use TypeScript in the context of React and assumes basic knowledge about React.
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!
Let’s say we have a JsInput component that is a simple wrapper around an input field. It could look like this:
This 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.
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 key's 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!
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 Inputcomponent, 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.
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:
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?”
Don't worry, you're 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're starting to feel safer, 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?
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 GenericInputinterface.
That T is then narrowed down when we define GenericTSInput: it extendsInputValue, 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:
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:
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:
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!
Get expert insights & news on the latest promotion trends in our monthly newsletter.
CEO & Founder