Getting Started with React and TypeScript
React and TypeScript makes for a fantastic combination when it comes to developing web sites and web applications. With React's focus on creating a fantastic user interface and TypeScript's ability to type check our code, it's never been easier to sanely develop a fantastic user experience.
Prerequisites
Before getting started, you'll need NodeJS and NPM installed. Go here to find instructions on how to install it on your computer.
You should also have a bit of knowledge in using React. If not, I highly recommend getting started learning React using vanilla JavaScript.
Create React App
Facebook's Create-React-App is a preview of the wonderful development experience React has to offer. It will enable us to start writing code immediately.
Check out their documentation here if you have any questions or want to dive in a touch deeper on how the tool works.
In summary, Create-React-App will set up a complete development environment for us. It will configure and conceal build tool configurations for us so that all we need to focus on is our code. Here is some of the tooling that CRA (create-react-app) sets up for us:
Let's use this tool now to create our TypeScript/React development environment. Use the following command to generate a new Typescript React App.
npx create-react-app my-app --template typescript
If you use yarn, use the following command:
yarn create react-app my-app --template typescript
After the tool has finished running, switch directories into my-app
run the command npm start
to spin up your development environment. Once it's done loading, we should see localhost:3000 open up.
Folder Structure
Create-react-app scaffolds out the folder structure of your app for us, minimizing the amount of code you need to write to go from idea to production.
Here are some important files to take note of:
public/index.html
- This is the HTML file that consumes the bundle generated by Create-React-App's hidden tooling.src/index.tsx
- This is the TypeScript entry point file. All of the project's dependencies are mapped out by Webpack (all set up for us invisibly by CRA) using this file. The bundle generated is injected intopublic/index.html
. This file has a few responsibilities. The most important are:import
our main React component- Render that App component to the DOM using
ReactDom.render()
src/App.tsx
- This is our main React Component. The.tsx
extension tells us that the file uses both TypeScript and JSX. This is the main component in our app which is rendered to the DOM by ReactDOM. We'll spend most of our time in this file getting the hang of writing React code using TypeScript.tsconfig.json
- This is the TypeScript configuration file. This file was generated for us by Create-React-App and we can freely edit this configuration to best suit our needs.
Hello, World - With TypeScript and React
Let's get to coding.
To keep things organized, make a new folder src/components
and create our new component, src/components/HelloWorld.tsx
. In this file, we'll write just 2 lines of code:
const HelloWorld = () => <h1>Hello, World!</h1>
export default HelloWorld
The first line defines our new component. It's simply a function that returns some JSX. In this case, it's our "Hello, World!" heading tag. The second line exports the component, allowing other modules to import it.
To import and use this component, load up src/App.tsx
and do the following:
- Include
import HelloWorld from './components/HelloWorld'
at the very top of the file to import the component - Use the component by having the App component return some JSX which includes
<HelloWorld />
.
After some refactoring, your App.tsx
might look something like this:
import './App.css';
import HelloWorld from './components/HelloWorld'
const App = () => {
return (
<div className="App">
<HelloWorld />
</div>
);
}
export default App;
In the above example, I've left the default styles, imported our HelloWorld component, and refactored the JSX which is returned by App
so it includes <HelloWorld />
.
Adding a bit of TypeScript
Let's increase the safety of our code by using some TypeScript features.
This is what our src/components/HelloWorld.tsx
looks like right now:
const HelloWorld = () => <h1>Hello, World!</h1>
export default HelloWorld
A huge benefit of TypeScript is that it can help us catch errors before you run your code instead of at run-time. Let's introduce a bug to our code:
Refactor our HelloWorld component so it looks like this:
const HelloWorld = () => "Hello, World!"
export default HelloWorld
This is valid JavaScript, but not valid React. A functional component must return JSX (or null). It can't just return a string. We're made aware of this error when Create-React-App tries to build our code.
Let's refactor our code to use some TypeScript features so our editor notices the error:
import { FC } from 'react'
const HelloWorld: FC = () => "Hello, World!"
export default HelloWorld
The import
statement imports the FunctionComponent type FC
, and allows us to type our functional component to ensure it returns valid JSX. By typing the function, our editor can now immediately show us our error. Click here for more information on typing functions in TypeScript.
TypeScript is explicitly telling us that we can't assign a return type of string
to our functional component. Now that TypeScript has shown us this error, let's correct it.
import { FC } from 'react'
const HelloWorld: FC = () => <h1>Hello, World!</h1>
export default HelloWorld
By fixing our function and returning JSX, HelloWorld
now passes TypeScript's type checks and no error is thrown. As a result, our project builds and works once more.
Benefits of TypeScript
As shown in the previous example, TypeScript can help us catch errors before we even leave our code editor. This results in faster development since TypeScript will be actively debugging your code as you write. Better to catch silly errors immediately than insanity-inducing errors during run-time.
Let's show another example of how enforcing types can help us catch errors early.
We'll create a new React component, List
. This new component will take an array of string data as a prop and return some JSX which renders a <ul>
tag and iterate over the list of data, rendering an <li>
for each piece of data.
Here is what my version of this component could look like with more type safety from TypeScript included.
src/components/List.tsx
import { FC } from 'react'
const List: FC<{ data: string[] }> = ({ data }) => (
<ul>
{data.map(item => <li>{item}</li>)}
</ul>
)
export default List
Just like our previous example, we import FC
from react
and use it to type our function. What we're also doing is defining a type for our data
prop. In this example, we are saying that data
is an array of strings. Notice how self-documenting that is. This is a huge benefit of TypeScript.
Now let's insert this component into src/App.tsx
, pass into it an array of data, and sprinkle in a bit more TypeScript.
src/App.tsx
import './App.css';
import { FC } from 'react'
import HelloWorld from './components/HelloWorld'
import List from './components/List'
const avengers = [
'Captain America',
'Iron Man',
'Black Widow',
'Thor',
'Hawkeye',
]
const App: FC = () => {
return (
<div className="App">
<HelloWorld />
<List data={avengers} />
</div>
);
}
export default App;
We created an avengers
array, with 5 members of the Avengers team. We then passed in this list of data as a property to List
. This example works because we defined List
to accept an array of strings. Let's trigger a TypeScript error and send it a more varied array.
import './App.css';
import { FC } from 'react'
import HelloWorld from './components/HelloWorld'
import List from './components/List'
const avengers = [
'Captain America',
'Iron Man',
'Black Widow',
'Thor',
'Hawkeye',
{
name: 'Vision',
},
{
name: 'Hulk',
color: 'green',
},
]
const App: FC = () => {
return (
<div className="App">
<HelloWorld />
<List data={avengers} />
</div>
);
}
export default App;
We added 2 new heroes to the avengers
array, Vision and the Hulk, but we added them as objects, not as strings. This causes TypeScript to throw an error.
TypeScript is telling us that our data doesn't fit the definition of the data
prop. It even points out to us exactly where this prop is defined. Let's assume that we should be sending an array of objects instead of strings. Let's refactor our files so everything works again.
src/App.tsx
import './App.css';
import { FC } from 'react'
import HelloWorld from './components/HelloWorld'
import List from './components/List'
const avengers = [
{ name: 'Captain America' },
{ name: 'Iron Man' },
{ name: 'Black Widow' },
{ name: 'Thor' },
{ name: 'Hawkeye' },
{ name: 'Vision' },
{ name: 'Hulk' },
]
const App: FC = () => {
return (
<div className="App">
<HelloWorld />
<List data={avengers} />
</div>
);
}
export default App;
src/components/List.tsx
import { FC } from 'react'
const List: FC<{ data: { name: string }[] }> = ({ data }) => (
<ul>
{data.map(({ name }) => <li>{name}</li>)}
</ul>
)
export default List
We changed our List
component definition and explicitly typed the data prop to be an array of objects containing a string type name property. By explicitly typing our data
prop, we know its exact shape and can catch if we send this component any prop that is shaped differently than this.
Conclusion
By adding a type layer over JavaScript, TypeScript successfully saves developers time and frustration, even giving us a hand in self-documenting our code. While there surely is much more left to learn in typing your React applications, this should be a good platform to begin wading into the deep end of the pool, so to speak.
Happy coding!