Getting Started with TypeScript in React
by Dominic van Almsick on 14th Sep 2023
by Dominic van Almsick on 14th Sep 2023
function BlogPost({ title }: { title: string }) {
return <h1>{title}</h1>;
}
<BlogPost title="Working on it..." />;
We cover the essentials you need to start using TypeScript effectively with React. This is not a comprehensive TypeScript guide, and in most (surely all) cases, the TypeScript docs provide better and more in-depth explanations.
The goal is to get you using simple TypeScript in your React projects as painlessly as possible.
You should already have a solid understanding of JavaScript and React. Some familiarity with basic TypeScript will also be helpful.
TypeScript is a superset of JavaScript. The implication is that any valid JavaScript is valid TypeScript.
TypeScript is JavaScript plus the ability to specify types for variables, function parameters, and more. To quote the TypeScript docs:
JavaScript provides language primitives like string and number, but it doesn’t check that you’ve consistently assigned these. TypeScript does.
When I first came across TypeScript, I was confused about its exact relationship to JavaScript. Is it a completely separate programming language? Does it have its own runtime? If you're similarly unsure, the following points might shed some light:
Let's take the simplest case. We have a variable month
, to which we want to assign the appropriate integer value
for a given month. E.g. 9
for September.
let month = 9; // September
We can imagine that another developer, or our future selves, might be tempted to re-assign this variable like so
month = 'October';
If we intended for month
to be used in a mathematical operation, we've now got a bug. For example, if we increment month:
month++; // month is now NaN
Number.isNaN(month) === true; // yep
This is legal JavaScript; we'll get no clues as to which code introduced the bug. We have to execute code in our heads until we identify where we went wrong. JavaScript alone doesn't surface the source of the problem.
TypeScript's purpose is to catch bugs like these during the code authoring process, reducing time and cognitive load spent on debugging,
Let's re-introduce our month
variable with TypeScript specific syntax:
let month: number = 9;
month = 'October';
Adding : number
to the left of the assignment operator explicitly tells TypeScript that month
should be a number.
When we then try to assign a string to month
we will immediately get a warning in our IDE. You can experiment with this example
in the TypeScript playground.
For this example, we didn't need to add extra syntax to get the benefits of type checking. Whenever possible, TypeScript gets out of our way by using what is called type inference, inferring the type from the initial assignment. Best-practice is to avoid adding type annotations when inference is straightforward.
As we saw above, TypeScript has no trouble inferring types for simple variables without our help. However, in JavaScript we spend much of our time working with objects.
We usually have a specific interface in mind for objects we create, even if we are not explicit about it. What properties should the object have? What types are
their values? What operations are permitted on them? Take the following objects, user1
and user2
:
const user1 = {
name: 'Dom',
isAdmin: true,
greeting: 'hello!',
birthday: 694224000000,
};
const user2 = {
name: 'Mick',
greeting(message) {
console.log(`Hi, ${message}`);
},
birthday: '1999-02-04',
};
Now reflect on what the following lines of code evaluate to. Which ones crash our program? Which won't? Are these helpful outcomes?
user1.isAdmin; // true
user2.isAdmin; // undefined
user1.isAdmin(); // TypeError: user1.isAdmin is not a function
user1.greeting; // 'hello!'
user2.greeting; // f greeting()
user1.birthday - user2.birthday; // NaN
user2.isAdmin
: accessing properties that don't exist is legal JavaScript. You get undefined
.
Have we forgotten to assign a value? Is the value intentionally undefined? It seems harmless, but what
if you're passing this data to a database? What type of data does the isAdmin
column expect?
Getting this wrong will cause bugs.
user2.isAdmin()
: it would be reasonable if .isAdmin()
were a method, particularly if it involves reading
from a database or user session. The error message is descriptive, making it easier to identify the source of the issue. Nevertheless,
it would be nice to avoid mistakes like this altogether.
user2.greeting
: we get a reference to the .greeting()
method. Let's say we tried this:
// assuming this exists on our web page
const h2 = document.querySelector('h2');
if (user2.greeting) {
h2.textContent = greeting;
} else {
h2.textContent = 'Hi there!';
}
Now we're displaying a function signature in our UI! I actually saw this happen on a payment confirmation page recently.
user1.birthday - user2.birthday
: calculating the time between dates is a common
task. Without knowing their types we have to search our code for the source of NaN
.
Here is the syntax for declaring a custom type and declaring that an object instance should conform to it:
type UserProps = {
name: string;
greeting: string;
birthday: number;
isAdmin: boolean;
};
const user1: UserProps = {
name: 'Dom',
isAdmin: true,
greeting: 'hello!',
birthday: 694224000000,
};
You can experiment with this example in the TypeScript playground. The syntax looks a lot like a regular JavaScript object, except types take the place of values. Note the semi-colons separating properties. This is common practice, but commas are also valid.
The workflow of declaring a custom type and specifying that a variable should conform to it is exactly how we will type component props in React.
We want to add a posts
property to our user object. posts
should be an array where each element is an object
containing data related to a user's blog post. Let's expand our example to reflect this. You can experiment with this
example in the TypeScript playground.
type PostProps = {
title: string;
author: string;
publishedOn: string;
};
type UserProps = {
name: string;
isAdmin: boolean;
greeting: string;
birthday: number;
posts: PostProps[];
};
const user1: UserProps = {
name: 'Dom',
isAdmin: true,
greeting: 'hello!',
birthday: 694224000000,
posts: [
{
title: 'Getting started with TypeScript in React',
author: 'Dominic van Almsick',
publishedOn: '2023-09-14',
},
{
title: 'Build a conditionally formatted calendar in React',
author: 'Dominic van Almsick',
publishedOn: '2023-10-12',
},
],
};
What have we added here?
PostProps
specifying the properties and types that post objects should haveposts
field to UserProps
posts: PostProps[];
should be read as:
the value for the posts property should be an array where each element is of type PostProps
The syntax for an array is the []
square brackets after the type name. In this example we used a custom
type. If we were specifying arrays of primitives the syntax would be:
const numbers: number[] = [1, 2, 3];
const strings: string[] = ['a', 'b', 'c'];
const bools: boolean[] = [true, false, false];
// etc.
We might want to specify that an object property is optional.
Sometimes it will be there, other times not. We can achieve this by including a question mark ?
before the colon :
.
in the type declaration. For example
type Person = {
name: string;
nickname?: string;
};
const person1: Person = {
name: 'Dom',
};
const person2: Person = {
name: 'Michael',
nickname: 'Mickey',
};
TypeScript will give us an error if we try to access a method on the nickname
property
unless we guard against it possibly being undefined.
// TypeScript won't like this
person1.nickname.toUpperCase();
// This will make it happy
person1.nickname?.toUpperCase();
This is really helpful for avoiding the dreaded Cannot read properties of undefined
error!
When it comes to functions there are two pieces of type information to consider
Let's get straight to an example, incorporating the concepts we have covered so far.
Consider the following function, getWordsFromString
, which accepts a string of text as input,
and returns an array where each element is a word from the original string. For simplicity we will only
cover separating on a single space.
function getWordsFromString(text: string): string[] {
return text.split(' ');
}
This level of detail will get us a long way in terms of typing React components. Typing functions does go much deeper though. Here's a link to the relevant section of the docs if you're keen to learn more.
Let's get ready to write some React.
We are going to refactor some existing JavaScript React to use TypeScript features.
The page should display a list of countries and data associated with them.
Each country has its own card in the UI. Clone this repo to get the starter code.
The repo has two branches: main
and solution
. You'll be working through the exercise on the main branch.
Once you've opened up the repo in VSCode and run npm install
, run npm run dev
to start the local dev server.
We've got the usual suspects in a boilerplate React app. For simplicity I've kept all of the code inside App.tsx
,
and you won't have to worry about styling at all. Here's the contents of App.tsx
import './App.css';
import countries from './countries.json';
function CountryCard({ name, continents, population, capital, flags }) {
return (
<article className="country">
<img src={flags.svg} alt={flags.alt} className="country__flag" />
<div className="country__text-content">
<hgroup>
<h2 className="country__name">{name}</h2>
<h3 className="country__continent">{continents[0]}</h3>
</hgroup>
<div className="country__info-container">
<p className="country__info">
<span className="country__label">Capital city</span>
<span className="country__value">{capital}</span>
</p>
<p className="country__info">
<span className="country__label">Population</span>
<span className="country__value">
{population.toLocaleString()}
</span>
</p>
</div>
</div>
</article>
);
}
function CountriesList({ countries }) {
return (
<ul className="countries">
{countries.map(country => (
<li key={country.name}>
<CountryCard {...country} />
</li>
))}
</ul>
);
}
function App() {
return (
<main>
<h1 className="main__heading">Countries Of the World</h1>
<CountriesList countries={countries} />
</main>
);
}
export default App;
This is pure JavaScript. For the exercise, we will refactor to add TypeScript-specific features.
Also for your reference here's the json data that we're importing on line 2. The data is based on a response from the REST countries API.
[
{
"flags": {
"png": "https://flagcdn.com/w320/pf.png",
"svg": "https://flagcdn.com/pf.svg",
"alt": ""
},
"name": "French Polynesia",
"capital": "Papeetē",
"landlocked": false,
"population": 280904,
"continents": ["Oceania"]
},
{
"flags": {
"png": "https://flagcdn.com/w320/mf.png",
"svg": "https://flagcdn.com/mf.svg",
"alt": ""
},
"name": "Saint Martin",
"capital": "Marigot",
"landlocked": false,
"population": 38659,
"continents": ["North America"]
},
{
"flags": {
"png": "https://flagcdn.com/w320/ve.png",
"svg": "https://flagcdn.com/ve.svg",
"alt": "The flag of Venezuela is composed of three equal horizontal bands of yellow, blue and red. At the center of the blue band are eight five-pointed white stars arranged in a horizontal arc."
},
"name": "Venezuela",
"capital": "Caracas",
"landlocked": false,
"population": 28435943,
"continents": ["South America"]
},
{
"flags": {
"png": "https://flagcdn.com/w320/re.png",
"svg": "https://flagcdn.com/re.svg",
"alt": ""
},
"name": "Réunion",
"capital": "Saint-Denis",
"landlocked": false,
"population": 840974,
"continents": ["Africa"]
},
{
"flags": {
"png": "https://flagcdn.com/w320/sv.png",
"svg": "https://flagcdn.com/sv.svg",
"alt": "The flag of El Salvador is composed of three equal horizontal bands of cobalt blue, white and cobalt blue, with the national coat of arms centered in the white band."
},
"name": "El Salvador",
"capital": "San Salvador",
"landlocked": false,
"population": 6486201,
"continents": ["North America"]
},
{
"flags": {
"png": "https://flagcdn.com/w320/dm.png",
"svg": "https://flagcdn.com/dm.svg",
"alt": "The flag of Dominica has a green field with a large centered tricolor cross. The vertical and horizontal parts of the cross each comprise three bands of yellow, black and white. A red circle, bearing a hoist-side facing purple Sisserou parrot standing on a twig and encircled by ten five-pointed yellow-edged green stars, is superimposed at the center of the cross."
},
"name": "Dominica",
"capital": "Roseau",
"landlocked": false,
"population": 71991,
"continents": ["North America"]
},
{
"flags": {
"png": "https://flagcdn.com/w320/ke.png",
"svg": "https://flagcdn.com/ke.svg",
"alt": "The flag of Kenya is composed of three equal horizontal bands of black, red with white top and bottom edges, and green. An emblem comprising a red, black and white Maasai shield covering two crossed white spears is superimposed at the center of the field."
},
"name": "Kenya",
"capital": "Nairobi",
"landlocked": false,
"population": 53771300,
"continents": ["Africa"]
},
{
"flags": {
"png": "https://flagcdn.com/w320/mv.png",
"svg": "https://flagcdn.com/mv.svg",
"alt": "The flag of Maldives has a red field, at the center of which is a large green rectangle bearing a fly-side facing white crescent."
},
"name": "Maldives",
"capital": "Malé",
"landlocked": false,
"continents": ["Asia"]
}
]
Now open the app in a browser window and see what we get... blank screen? Check the console.
Uncaught TypeError: Cannot read properties of undefined (reading 'toLocaleString')
Classic.
Your tasks are to:
CountryCard
and CountriesList
should use itWe have introduced some essential TypeScript basics:
We have explored how we can apply these basic principles, and refactor React components to leverage their benefits.