React with TypeScript: Best Practices for Developers
In my experience as a web developer, one of the growing trends in software development is the combination of TypeScript and React. TypeScript, a statically typed superset of JavaScript, offers numerous benefits, such as improved code quality and easier tooling. React, on the other hand, is a widely-used library for building user interfaces. Combining these two technologies can result in cleaner, more efficient, and less error-prone code for your web applications.
While using React with TypeScript, I’ve found that integrating TypeScript’s strong typing system with React components makes catching errors earlier in the development process easier. Additionally, using TypeScript can improve code readability and maintainability. This is essential when working on large-scale applications. I’ve come across numerous tools and frameworks, such as Create React App, which support TypeScript out of the box, making it even more accessible to developers. One aspect I appreciate when working with TypeScript and React is the ability to type component props and hooks. This helps avoid common mistakes and ensures the components are used correctly throughout the application. With both TypeScript and React being highly popular technologies in the web development world, they are a powerful combination that can enhance the overall experience of building and managing complex software projects.
Setting Up React with TypeScript
Install TypeScript in a React project, Option 1 : using Create React App
To start using TypeScript with React, the preferred way used to be to create new React projects using create-react-app
and the --template typescript
flag:
npx create-react-app my-react-typescript-app --template typescript
This command sets up my project with TypeScript support, including the necessary .ts
and .tsx
files. Next, I need to install the @types/react
and @types/react-dom
packages:
npm install --save-dev @types/react @types/react-dom
These packages provide type definitions for React, making it easier to work with TypeScript.
Install TypeScript in a React project : using Vite
The React team now no longer recommends using Create React App.
The easiest way to go about things is to use Vite.
npm create vite@latest
Then simply specify the type of project you want to set up, e.g. a React project, using TypeScript and SWC.
Configuring tsconfig.json
After installing TypeScript, I configure my tsconfig.json
file, which is located in my project’s root directory. The default configuration provided by create-react-app
is sufficient for most use cases, but I can make changes if needed. The tsconfig.json
file is used to specify which .ts
and .tsx
files should be included in my project and to define compiler options. Here’s a basic example of a tsconfig.json
configuration:
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "react-jsx"
},
"include": ["src"]
}
Setting up Visual Studio Code
I use Visual Studio Code as it provides excellent support for TypeScript out of the box. All I have to do is open my project in VSCode, and it will automatically recognise .ts
and .tsx
files.
Setting up TypeScript linting
Proper linting ensures my TypeScript code follows best practices. I begin by installing eslint
and the necessary plugins for React and TypeScript:
npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react
Next, I create an .eslintrc.js
file in my project’s root directory to configure linting rules. A minimal configuration might look like this:
module.exports = {
parser: "@typescript-eslint/parser",
plugins: ["@typescript-eslint", "react"],
extends: [
"eslint:recommended",
"plugin:@typescript-eslint/recommended",
"plugin:react/recommended"
],
settings: {
react: {
version: "detect"
}
},
rules: { // Add or modify rules as necessary
}};
Now, with my React and TypeScript project set up, I can begin writing components using .tsx
files and enjoy the benefits of type-safe code.
TypeScript in React Components
Using TypeScript in React components not only improves the code quality but also enhances the development experience. In this section, we’ll cover how to define functional components type, props type in functional components, and working with TypeScript in class components. But first we need to understand what we are doing, and for that we need to take a look at a TypeScript feature called “Generics”.
Understanding Generics
Every time we define a type that uses the React API, we’ll be using a feature called Generics. What exactly are Generics in TypeScript, and why are they so crucial for our work?
Generics are a way to make certain parts of your code more flexible and reusable without losing the safety net of types.
Think about a React functional component. It has very specific specific requirements for its output : it has to return React content. However it doesn’t have any limitation regarding its input.
Generics is a way to do that, it allows the React team to define the output, and leave the responsibility of defining the input to the developper.
It's like creating a template for your functions, components, or interfaces, where you can specify the types later when you actually use them.
Let's look at a basic example. Suppose you have a function that returns the first item of an array. Without Generics, this function might return any type, losing specific type information. With Generics, you can specify the type of items in the array, keeping the function versatile and type-safe.
function getFirstItem<T>(items: T[]): T {
return items[0];
}
// Usage
const firstNumber = getFirstItem<number>([1, 2, 3]);
const firstName = getFirstItem<string>(["Alice", "Bob", "Charlie"]);
In this example, <T>
is a placeholder for the type, which we specify when we call the function. This way, TypeScript knows exactly what type of value getFirstItem
returns in each case.
By using Generics, we ensure our code is adaptable and robust. It helps prevent errors by maintaining type consistency across our components and functions, making our React applications more reliable and easier to maintain.
Now that we understand Generics, let's see how they play a pivotal role in defining types for functional components, managing state with hooks, and more in our React applications with TypeScript.
Using Functional Components Type & Prop Interfaces
When defining functional components, use the React.FC
type (short for React.FunctionComponent) to define your components. This way, you can leverage TypeScript’s static typing and have better type checking for your component props.
By using React.FC
I can make sure that my component handles the children
prop properly and that it returns a valid ReactNode type.
To define the prop types for functional components, I use TypeScript interfaces as the Generics parameter of my React.FC
type. This provides better type checking and autocompletion for the props, ensuring that my components receive the correct data. Here’s an example:
interface ButtonProps {
text: string;
onClick: (event: React.MouseEvent<HTMLButtonElement>) => void;
disabled?: boolean;
}
const Button: React.FC<ButtonProps> = ({ text, onClick, disabled = false }) => {
return (
<button onClick={onClick} disabled={disabled}>
{" "}
{text}{" "}
</button>
);
};
With these interfaces, I can define optional props, defaultProps, and specify the types of event handlers like onClick
.
TypeScript with Class Components
When working with class components, TypeScript can be used to ensure proper typing and validation. I do this by defining the component props and state types on React.Component
(again, using Generics):
import React from "react";
interface CounterProps {
initialCount: number;
}
interface CounterState {
count: number;
}
class Counter extends React.Component<CounterProps, CounterState> {
constructor(props: CounterProps) {
super(props);
this.state = { count: props.initialCount };
}
increment = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
{" "}
<h1>Count: {this.state.count}</h1>{" "}
<button onClick={this.increment}>Increment</button>{" "}
</div>
);
}
}
By using TypeScript with class components, I can improve code completion, validation, and maintainability in my React app.
We help you better understand software development. Receive the latest blog posts, videos, insights and news from the frontlines of web development
Working with React Hooks and TypeScript
useState
useState is the most commonly used hook. It allows you to manage local state in functional components. With TypeScript (and using Generics again), it is essential to provide a type for my state so that Intellisense can give me better suggestions and I can avoid runtime errors. For example, if I want a simple counter, I use the number
type:
const [count, setCount] = useState<number>(0);
By specifying the type, TypeScript will ensure that only numbers are updated in the state.
useEffect
TypeScript can help me catch potential errors in my implementation in useEffect, like using the wrong type in the dependencies. However there is nothing specific to declare in TypeScript for the useEffect hook, since it does not return anything and therefore does not have any direct effect on the code (Which is the whole point of a side effect hook, to be fair).
useContext
The useContext hook takes a context object created by React.createContext
, and when used with TypeScript, I define the interface that represents the shape of the context.
interface MyContext {
property: string;
}
const MyContext = React.createContext<MyContext>({
property:""
});
const { property } = useContext(MyContext);
useRef
When I need to interact with DOM elements or store mutable values in my components without causing re-renders, I use the useRef hook. useRef can store any value, so provide the specific type, with a type union with null
if the initial value is null
.
const inputRef = useRef<HTMLInputElement|null>(null);
By providing the type for the ref, I am ensured that I can only interact with the correct HTML element type.
useReducer
The useReducer hook allows me to manage more complex state and side effects. With TypeScript, I define the state and action interfaces to ensure type safety in my reducer function. Here’s an example:
interface CounterState {
count: number;
}
type CounterAction = { type: "increment" | "decrement" };
function counterReducer(
state: CounterState,
action: CounterAction
): CounterState {
switch (action.type) {
case "increment":
return { count: state.count + 1 };
case "decrement":
return { count: state.count - 1 };
default:
return state;
}
}
const initialState: CounterState = { count: 0 };
const [state, dispatch] = useReducer(counterReducer, initialState);
By thoroughly defining my types, I can take full advantage of TypeScript’s features and ensure that my hooks are used correctly and effectively in my React components.
Advanced TypeScript Features in React
In this section, I will discuss some of the advanced TypeScript features that can be used in React projects. These features enhance our development experience and help us write more efficient and maintainable code.
Type Guards and Inference
Type guards are a powerful feature in TypeScript that allows me to narrow down the types of values in a specific context. This can be especially useful when working with union types or any
values to ensure correct usage and prevent runtime errors. TypeScript’s type inference engine can derive types from variable assignments, function return values, and other situations.
Let’s come back to our reducer example and beef it up a little. Imagine, for example, that we’re our counter is in fact a score, and we are also tracking a gameState
which can take several values.
Our game state now looks like this
type GameStates = "won" | "lost" | "in progress" | "not started";
interface GameState {
score: number;
gameState: GameStates;
}
Given this state, what are the actions we’d like to carry out ?
Well, we can of course increment or decrement the score, maybe by a given about.
In that case we can define an action for that :
type ScoreAction = {
actionType: "changeScore";
score: number
}
We can also define an action to set the gameState:
type GameStateAction = {
actionType: "setGamestate";
gameState: GameStates
}
Now we define the actions as a union of the different types :
type Action = GameStateAction | ScoreAction;
And now, we can use the “actionType” field, and TypeScript is able to infer the action type based on the value of the “actionType” field.
If actionType is “changeScore”, TypeScript knows we have a ScoreAction, conversely if actionType is setGamestate, TypeScript knows the action is a GameStateAction.
function counterReducer(
state: GameState,
action: Action
): CounterState {
switch (action.actionType) {
case "changeScore":
// we know we have a score action with an amount
return { ...state, score: state.score + action.score};
case "setGamestate":
// we know we have a state action with a value field
return { ...state, gameState: action.gameState };
default:
return state;
}
}
const initialState: GameState = { score: 0, gameState: "not started" };
const [state, dispatch] = useReducer(counterReducer, initialState);
TypeScript can correctly infer that the action
is an GameStateAction or a ScoreAction inside the if
block and provides better IDE support with autocompletion.
Using Utility Types
Utility types in TypeScript allow me to transform and manipulate the types of my React components and props. They are incredibly helpful for making my TypeScript code more flexible and reusable. For example, in a React project, I might have a Props
interface for a component:
interface Props { title: string; subtitle?: string;}
Now, if I want to make all properties of Props
optional, I can use the Partial
utility type:
import React from "react";
type PartialProps = Partial<Props>;
function MyComponent({ title, subtitle }: PartialProps) {
return (
<div>
<h1>{title || "Default Title"}</h1>
{subtitle && <h2>{subtitle}</h2>}
</div>
);
}
Another useful utility type is Pick
. It allows me to create a new type by selecting specific properties from an existing one:
import React from "react";
type TitleProps = Pick<Props, "title">;
function TitleComponent({ title }: TitleProps) {
return <h1>{title}</h1>;
}
By leveraging advanced TypeScript features such as type guards, inference, and utility types in my React projects, I can ensure the reliability and maintainability of my code while enjoying a more productive development experience.
Tips and Best Practices for Using React and TypeScript
As someone who has worked with React and TypeScript, I’d like to share some of the tips and best practices I’ve gathered from my experiences: managing 3rd party libraries and types and improving debugging and error handling.
Managing 3rd Party Libraries and Types
When using TypeScript with React, managing 3rd party libraries and their type definitions effectively is important. To improve the overall development experience, I ensure that I work with popular libraries that have TypeScript typings or .d.ts
files included. If typings are not included, I search for and install them from the DefinitelyTyped repository using npm
or yarn
. For example, when installing the axios
library, I also install the corresponding type definitions with:
npm install axios
npm install @types/axios --save-dev
I also pay attention to the documentation of each library and follow their recommended practices for using TypeScript. This helps me avoid common bugs and ensures a smoother development process.
Debugging and Error Handling
Debugging and error handling are critical when working with React and TypeScript. I use source maps with my bundler (e.g., Webpack) to map the compiled JavaScript code back to the original TypeScript code. This makes my debugging process more straightforward and enables me to trace errors back to the TypeScript code. When working with React components, I use TypeScript’s built-in JSX support to use the .tsx
extension in my files instead of .ts
. This helps ensure better type support for React-specific syntax and makes it easier for me to identify errors related to TypeScript types or module imports. In addition to strict typing, I use React’s built-in Error Boundaries to handle unexpected errors and gracefully display a fallback UI. This approach is particularly useful for providing a good user experience when errors occur during runtime. Furthermore, I make use of my browser’s developer tools and integrated development environment (IDE) tools to effectively detect, locate, and resolve errors. Combined with TypeScript’s strict typing and React’s error-handling techniques, these tools contribute to a cleaner and more maintainable codebase. By applying these tips and best practices, I have found that my React and TypeScript projects run more smoothly, and my development experience dramatically improves.
FAQ
What are the benefits of using TypeScript?
The main benefits of using TypeScript with React include improved code quality, better developer experience, and easier maintainability. TypeScript’s type system helps catch errors early, enables code refactoring, and provides better navigation and intellisense features. Additionally, using TypeScript can make your code more self-documenting and easier to understand for other developers.
How to set up create-react-app with TypeScript?
Setting up create-react-app with TypeScript is simple. Just run the following command: npx create-react-app my-app --template typescript
. This will generate a new React project with TypeScript support out of the box. For more information, check the official TypeScript documentation.
What are some React TypeScript example projects?
There are many popular frameworks that support TypeScript, such as Next.js , Remix and Gatsby. These frameworks provide practical examples and boilerplates for using TypeScript with React. The TypeScript-React-Starter by Microsoft on GitHub is also a good starting point for a React TypeScript example project.
How to create functional components with TypeScript?
To create a functional component with TypeScript, you need to define its prop types using interfaces or types. Then, you can use these prop types to declare your functional component. Here is a simple example:
import React from "react";
interface Props {
message: string;
}
const MyComponent: React.FC<Props> = ({ message }) => {
return <div>{message}</div>;
};
In this example, Props
is an interface defining the expected prop message
. The MyComponent
functional component takes this prop and renders a div
element containing the message.
Where can I find React TypeScript documentation?
The official TypeScript documentation includes a dedicated section for React, which you can find here. This section provides useful information on setting up React projects with TypeScript, creating functional components, and working with React hooks in TypeScript.
What are the best practices for TypeScript in React?
Some best practices for using TypeScript in React projects include:
- Define prop types for your components using TypeScript interfaces or types.
- Prefer using functional components and hooks over class components.
- Avoid using
any
type, and be as specific as possible with your types. - Use utility types such as
Partial
,Readonly
, andPick
to map and manipulate your component prop types. - Write type annotations for event handlers and other React-specific code.