React with TypeScript: Best Practices

Share this article

React with TypeScript: Best Practices

React and TypeScript are two awesome technologies used by a lot of developers these days. Knowing how to do things can get tricky, and sometimes it’s hard to find the right answer. Not to worry. We’ve put together the best practices along with examples to clarify any doubts you may have.

Let’s dive in!

How React and TypeScript Work Together

Before we begin, let’s revisit how React and TypeScript work together. React is a “JavaScript library for building user interfaces”, while TypeScript is a “typed superset of JavaScript that compiles to plain JavaScript.” By using them together, we essentially build our UIs using a typed version of JavaScript.

The reason you might use them together would be to get the benefits of a statically typed language (TypeScript) for your UI. This means more safety and fewer bugs shipping to the front end.

Does TypeScript Compile My React Code?

A common question that’s always good to review is whether TypeScript compiles your React code. The way TypeScript works is similar to this interaction:

TS: “Hey, is this all your UI code?”
React: “Yup!”
TS: “Cool! I’m going to compile it and make sure you didn’t miss anything.”
React: “Sounds good to me!”

So the answer is yes, it does! But later, when we cover the tsconfig.json settings, most of the time you’ll want to use "noEmit": true. What this means is TypeScript will not emit JavaScript out after compilation. This is because typically, we’re just utilizing TypeScript to do our type-checking.

The output is handled, in a CRA setting, by react-scripts. We run yarn build and react-scripts bundles the output for production.

To recap, TypeScript compiles your React code to type-check your code. It doesn’t emit any JavaScript output (in most scenarios). The output is still similar to a non-TypeScript React project.

Can TypeScript Work with React and webpack?

Yes, TypeScript can work with React and webpack. Lucky for you, the webpack documentation has a guide on that.

Hopefully, that gives you a gentle refresher on how the two work together. Now, on to best practices!

Best Practices

We’ve researched the most common questions and put together this handy list of the most common use cases for React with TypeScript. This way, you can use this article as a reference in your own projects.

Configuration

One of the least fun, yet most important parts of development is configuration. How can we set things up in the shortest amount of time that will provide maximum efficiency and productivity? We’ll discuss project setup including:

  • tsconfig.json
  • ESLint
  • Prettier
  • VS Code extensions and settings.

Project Setup

The quickest way to start a React/TypeScript app is by using create-react-app with the TypeScript template. You can do this by running:

npx create-react-app my-app --template typescript

This will get you the bare minimum to start writing React with TypeScript. A few noticeable differences are:

  • the .tsx file extension
  • the tsconfig.json
  • the react-app-env.d.ts

The tsx is for “TypeScript JSX“. The tsconfig.json is the TypeScript configuration file, which has some defaults set. The react-app-env.d.ts references the types of react-scripts, and helps with things like allowing for SVG imports.

tsconfig.json

Lucky for us, the latest React/TypeScript template generates tsconfig.json for us. However, they add the bare minimum to get started. We suggest you modify yours to match the one below. We’ve added comments to explain the purpose of each option as well:

{
  "compilerOptions": {
    "target": "es5", // Specify ECMAScript target version
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ], // List of library files to be included in the compilation
    "allowJs": true, // Allow JavaScript files to be compiled
    "skipLibCheck": true, // Skip type checking of all declaration files
    "esModuleInterop": true, // Disables namespace imports (import * as fs from "fs") and enables CJS/AMD/UMD style imports (import fs from "fs")
    "allowSyntheticDefaultImports": true, // Allow default imports from modules with no default export
    "strict": true, // Enable all strict type checking options
    "forceConsistentCasingInFileNames": true, // Disallow inconsistently-cased references to the same file.
    "module": "esnext", // Specify module code generation
    "moduleResolution": "node", // Resolve modules using Node.js style
    "isolatedModules": true, // Unconditionally emit imports for unresolved files
    "resolveJsonModule": true, // Include modules imported with .json extension
    "noEmit": true, // Do not emit output (meaning do not compile code, only perform type checking)
    "jsx": "react", // Support JSX in .tsx files
    "sourceMap": true, // Generate corrresponding .map file
    "declaration": true, // Generate corresponding .d.ts file
    "noUnusedLocals": true, // Report errors on unused locals
    "noUnusedParameters": true, // Report errors on unused parameters
    "incremental": true, // Enable incremental compilation by reading/writing information from prior compilations to a file on disk
    "noFallthroughCasesInSwitch": true // Report errors for fallthrough cases in switch statement
  },
  "include": [
    "src/**/*" // *** The files TypeScript should type check ***
  ],
  "exclude": ["node_modules", "build"] // *** The files to not type check ***
}

The additional recommendations come from the react-typescript-cheatsheet community and the explanations come from the Compiler Options docs in the Official TypeScript Handbook. This is a wonderful resource if you want to learn about other options and what they do.

ESLint/Prettier

In order to ensure that your code follows the rules of the project or your team, and the style is consistent, it’s recommended you set up ESLint and Prettier. To get them to play nicely, follow these steps to set it up.

  1. Install the required dev dependencies:
    yarn add eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react --dev
    
  2. Create a .eslintrc.js file at the root and add the following:
    module.exports =  {
      parser:  '@typescript-eslint/parser',  // Specifies the ESLint parser
      extends:  [
        'plugin:react/recommended',  // Uses the recommended rules from @eslint-plugin-react
        'plugin:@typescript-eslint/recommended',  // Uses the recommended rules from @typescript-eslint/eslint-plugin
      ],
      parserOptions:  {
      ecmaVersion:  2018,  // Allows for the parsing of modern ECMAScript features
      sourceType:  'module',  // Allows for the use of imports
      ecmaFeatures:  {
        jsx:  true,  // Allows for the parsing of JSX
      },
      },
      rules:  {
        // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
        // e.g. "@typescript-eslint/explicit-function-return-type": "off",
      },
      settings:  {
        react:  {
          version:  'detect',  // Tells eslint-plugin-react to automatically detect the version of React to use
        },
      },
    };
    
  3. Add Prettier dependencies:
    yarn add prettier eslint-config-prettier eslint-plugin-prettier --dev
    
  4. Create a .prettierrc.js file at the root and add the following:
    module.exports =  {
      semi:  true,
      trailingComma:  'all',
      singleQuote:  true,
      printWidth:  120,
      tabWidth:  4,
    };
    
  5. Update the .eslintrc.js file:
    module.exports =  {
      parser:  '@typescript-eslint/parser',  // Specifies the ESLint parser
      extends:  [
        'plugin:react/recommended',  // Uses the recommended rules from @eslint-plugin-react
        'plugin:@typescript-eslint/recommended',  // Uses the recommended rules from the @typescript-eslint/eslint-plugin
    +   'prettier/@typescript-eslint',  // Uses eslint-config-prettier to disable ESLint rules from @typescript-eslint/eslint-plugin that would conflict with prettier
    +   'plugin:prettier/recommended',  // Enables eslint-plugin-prettier and displays prettier errors as ESLint errors. Make sure this is always the last configuration in the extends array.
      ],
      parserOptions:  {
      ecmaVersion:  2018,  // Allows for the parsing of modern ECMAScript features
      sourceType:  'module',  // Allows for the use of imports
      ecmaFeatures:  {
        jsx:  true,  // Allows for the parsing of JSX
      },
      },
      rules:  {
        // Place to specify ESLint rules. Can be used to overwrite rules specified from the extended configs
        // e.g. "@typescript-eslint/explicit-function-return-type": "off",
      },
      settings:  {
        react:  {
          version:  'detect',  // Tells eslint-plugin-react to automatically detect the version of React to use
        },
      },
    };
    

These recommendations come from a community resource written called “Using ESLint and Prettier in a TypeScript Project”, by Robert Cooper. If you visit this resource, you can read more about the “why” behind these rules and configurations.

VS Code Extensions and Settings

We’ve added ESLint and Prettier and the next step to improve our DX is to automatically fix/prettify our code on save.

First, install the ESLint extension and the Prettier extension for VS Code. This will allow ESLint to integrate with your editor seamlessly.

Next, update your Workspace settings by adding the following to your .vscode/settings.json:

{
    "editor.formatOnSave": true
}

This will allow VS Code to work its magic and fix your code when you save. It’s beautiful!

These suggestions also come from the previously linked article “Using ESLint and Prettier in a TypeScript Project”, by Robert Cooper.

Note: to read more about React.FC, look here, and read here for React.ReactNode.

Components

One of the core concepts of React is components. Here, we’ll be referring to standard components as of React v16.8, meaning ones that use hooks as opposed to classes.

In general, there’s much to be concerned with for basic components. Let’s look at an example:

import React from 'react'

// Written as a function declaration
function Heading(): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// Written as a function expression
const OtherHeading: React.FC = () => <h1>My Website Heading</h1>

Notice the key difference here. In the first example, we’re writing our function as a function declaration. We annotate the return type with React.Node because that’s what it returns. In contrast, the second example uses a function expression. Because the second instance returns a function, instead of a value or expression, we annotate the function type with React.FC for React “Function Component”.

It can be confusing to remember the two. It’s mostly a matter of design choice. Whichever you choose to use in your project, use it consistently.

Props

The next core concept we’ll cover is props. You can define your props using either an interface or a type. Let’s look at another example:

import React from 'react'

interface Props {
  name: string;
  color: string;
}

type OtherProps = {
  name: string;
  color: string;
}

// Notice here we're using the function declaration with the interface Props
function Heading({ name, color }: Props): React.ReactNode {
  return <h1>My Website Heading</h1>
}

// Notice here we're using the function expression with the type OtherProps
const OtherHeading: React.FC<OtherProps> = ({ name, color }) =>
  <h1>My Website Heading</h1>

When it comes to types or interfaces, we suggest following the guidelines presented by the react-typescript-cheatsheet community:

  • “always use interface for public API’s definition when authoring a library or 3rd-party ambient type definitions.”
  • “consider using type for your React Component Props and State, because it is more constrained.”

You can read more about the discussion and see a handy table comparing types vs interfaces here.

Let’s look at one more example so we can see something a little bit more practical:

import React from 'react'

type Props = {
  /** color to use for the background */
  color?: string;
  /** standard children prop: accepts any valid React Node */
  children: React.ReactNode;
  /** callback function passed to the onClick handler*/
  onClick: ()  => void;
}

const Button: React.FC<Props> = ({ children, color = 'tomato', onClick }) => {
   return <button style={{ backgroundColor: color }} onClick={onClick}>{children}</button>
}

In this <Button /> component, we use a type for our props. Each prop has a short description listed above it to provide more context to other developers. The ? after the prop named color indicates that it’s optional. The children prop takes a React.ReactNode because it accepts everything that’s a valid return value of a component (read more here). To account for our optional color prop, we use a default value when destructuring it. This example should cover the basics and show you have to write types for your props and use both optional and default values.

In general, keep these things in mind when writing your props in a React and TypeScript project:

  • Always add descriptive comments to your props using the TSDoc notation /** comment */.
  • Whether you use types or interfaces for your component props, use them consistently.
  • When props are optional, handle appropriately or use default values.

Hooks

Luckily, the TypeScript type inference works well when using hooks. This means you don’t have much to worry about. For instance, take this example:

// `value` is inferred as a string
// `setValue` is inferred as (newValue: string) => void
const [value, setValue] = useState('')

TypeScript infers the values given to use by the useState hook. This is an area where React and TypeScript just work together and it’s beautiful.

On the rare occasions where you need to initialize a hook with a null-ish value, you can make use of a generic and pass a union to correctly type your hook. See this instance:

type User = {
  email: string;
  id: string;
}

// the generic is the < >
// the union is the User | null
// together, TypeScript knows, "Ah, user can be User or null".
const [user, setUser] = useState<User | null>(null);

The other place where TypeScript shines with Hooks is with userReducer, where you can take advantage of discriminated unions. Here’s a useful example:

type AppState = {};
type Action =
  | { type: "SET_ONE"; payload: string }
  | { type: "SET_TWO"; payload: number };

export function reducer(state: AppState, action: Action): AppState {
  switch (action.type) {
    case "SET_ONE":
      return {
        ...state,
        one: action.payload // `payload` is string
      };
    case "SET_TWO":
      return {
        ...state,
        two: action.payload // `payload` is number
      };
    default:
      return state;
  }
}

Source: react-typescript-cheatsheet Hooks section

The beauty here lies in the usefulness of discriminated unions. Notice how Action has a union of two similar-looking objects. The property type is a string literal. The difference between this and a type string is that the value must match the literal string defined in the type. This means your program is extra safe because a developer can only call an action that has a type key set to "SET_ONE" or "SET_TWO".

As you can see, Hooks don’t add much complexity to the nature of a React and TypeScript project. If anything, they lend themselves well to the duo.

Common Use Cases

This section is to cover the most common use cases where people stumble when using TypeScript with React. We hope by sharing this, you’ll avoid the pitfalls and even share this knowledge with others.

Handling Form Events

One of the most common cases is correctly typing the onChange used on an input field in a form. Here’s an example:

import React from 'react'

const MyInput = () => {
  const [value, setValue] = React.useState('')

  // The event type is a "ChangeEvent"
  // We pass in "HTMLInputElement" to the input
  function onChange(e: React.ChangeEvent<HTMLInputElement>) {
    setValue(e.target.value)
  }

  return <input value={value} onChange={onChange} id="input-example"/>
}

Extending Component Props

Sometimes you want to take component props declared for one component and extend them to use them on another component. But you might want to modify one or two. Well, remember how we looked at the two ways to type component props, types or interfaces? Depending on which you used determines how you extend the component props. Let’s first look at the way using type:

import React from 'react';

type ButtonProps = {
    /** the background color of the button */
    color: string;
    /** the text to show inside the button */
    text: string;
}

type ContainerProps = ButtonProps & {
    /** the height of the container (value used with 'px') */
    height: number;
}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

If you declared your props using an interface, then we can use the keyword extends to essentially “extend” that interface but make a modification or two:

import React from 'react';

interface ButtonProps {
    /** the background color of the button */
    color: string;
    /** the text to show inside the button */
    text: string;
}

interface ContainerProps extends ButtonProps {
    /** the height of the container (value used with 'px') */
    height: number;
}

const Container: React.FC<ContainerProps> = ({ color, height, width, text }) => {
  return <div style={{ backgroundColor: color, height: `${height}px` }}>{text}</div>
}

Both methods solve the problem. It’s up to you to decide which to use. Personally, extending an interface feels more readable, but ultimately, it’s up to you and your team.

You can read more about both concepts in the TypeScript Handbook:

Third-party Libraries

Whether it’s for a GraphQL client like Apollo or for testing with something like React Testing Library, we often find ourselves using third-party libraries in React and TypeScript projects. When this happens, the first thing you want to do is see if there’s a @types package with the TypeScript type definitions. You can do so by running:

#yarn
yarn add @types/<package-name>

#npm
npm install @types/<package-name>

For instance, if you’re using Jest, you can do this by running:

#yarn
yarn add @types/jest

#npm
npm install @types/jest

This would then give you added type-safety whenever you’re using Jest in your project.

The @types namespace is reserved for package type definitions. They live in a repository called DefinitelyTyped, which is partially maintained by the TypeScript team and partially the community.

Should these be saved as dependencies or devDependencies in my package.json?

The short answer is “it depends”. Most of the time, they can go under devDependencies if you’re building a web application. However, if you’re writing a React library in TypeScript, you may want to include them as dependencies.

There are a few answers to this on Stack Overflow, which you may check out for further information.

What happens if they don’t have a @types package?

If you don’t find a @types package on npm, then you essentially have two options:

  1. Add a basic declaration file
  2. Add a thorough declaration file

The first option means you create a file based on the package name and put it at the root. If, for instance, we needed types for our package banana-js, then we could create a basic declaration file called banana-js.d.ts at the root:

declare module 'banana-js';

This won’t provide you type safety but it will unblock you.

A more thorough declaration file would be where you add types for the library/package:

declare namespace bananaJs {
    function getBanana(): string;
    function addBanana(n: number) void;
    function removeBanana(n: number) void;
}

If you’ve never written a declaration file, then we suggest you take a look at the guide in the official TypeScript Handbook.

Summary

Using React and TypeScript together in the best way takes a bit of learning due to the amount of information, but the benefits pay off immensely in the long run. In this article, we covered configuration, components, props, hooks, common use cases, and third-party libraries. Although we could dive deeper into a lot of individual areas, this should cover the 80% needed to help you follow best practices.

If you’d like to see this in action, you can see this example on GitHub.

If you’d like to get in touch, share feedback on this article or chat about using the two technologies together, you can reach me on Twitter @jsjoeio.

Further Reading

If you’d like to dive deeper, here are some resources we suggest:

react-typescript-cheatsheet

A lot of these recommendations came straight from the react-typescript-cheatsheet. If you’re looking for specific examples or details on anything React-TypeScript, this is the place to go. We welcome contributions as well!

Official TypeScript Handbook

Another fantastic resource is the TypeScript Handbook. This is kept up to date by the TypeScript team and provides examples and an in-depth explanation behind the inner workings of the language.

TypeScript Playground

Did you know you can test out React with TypeScript code right in the browser? All you have to do is import React. Here’s a link to get you started.

Practical Ways to Advance Your TypeScript Skills

Read our guide on practical ways to advance your TypeScript skills to set yourself up for continuous learning as you move forward.

FAQs About React with TypeScript

Can you use React with TypeScript?

es, you can absolutely use React with TypeScript. In fact, combining React with TypeScript has become increasingly popular in the web development community. TypeScript is a statically typed superset of JavaScript that provides enhanced tooling and type safety, making it an excellent choice for building robust and maintainable React applications.
When using React with TypeScript, you typically create React components as TypeScript classes or functional components with TypeScript function signatures. TypeScript allows you to define strong types for props and state, reducing the risk of runtime errors and making your codebase more predictable. Additionally, TypeScript’s autocompletion and type checking in modern code editors provide valuable assistance during development.
To start a React project with TypeScript, you can use tools like Create React App with TypeScript template or manually configure TypeScript in an existing React project. With TypeScript, you can enjoy the benefits of static typing while building dynamic and interactive user interfaces with React, resulting in more reliable and maintainable web applications.

Is TypeScript necessary for React?

No, TypeScript is not necessary for building React applications, but it can be highly beneficial. React was originally developed using JavaScript (ECMAScript), and many React applications are still written in plain JavaScript. React works seamlessly with JavaScript, and you can create fully functional and efficient React applications without TypeScript.
However, TypeScript can provide significant advantages when working with React. TypeScript is a statically typed superset of JavaScript, which means it adds type annotations and checking to JavaScript code. These type annotations can catch type-related errors at compile-time, offering improved code quality and maintainability. TypeScript can make large and complex React codebases more manageable by providing type safety for props, state, and function parameters, reducing the likelihood of runtime errors.
In summary, TypeScript is not a requirement for React, and you can use React effectively with plain JavaScript. However, TypeScript can enhance your development experience by adding type checking and improving code predictability, making it a valuable choice for building robust and maintainable React applications, especially in larger and more complex projects.

How to use TypeScript in React apps?

Start by setting up a new React project with TypeScript. You can use tools like Create React App with TypeScript template or manually configure TypeScript in an existing React project.
Next, write your React components using TypeScript. You can create functional components with TypeScript function signatures or use TypeScript classes for class components. TypeScript allows you to specify prop types and state types, providing strong type checking and autocompletion support in code editors. If you’re using third-party libraries or packages in your React app, make sure to install TypeScript type definitions for those dependencies. Many popular libraries have community-maintained TypeScript type declarations available on DefinitelyTyped

What Is the difference between React.js and React TypeScript?

The primary distinction between React.js and React TypeScript is the choice of programming language used for development.
React.js (JavaScript): React.js, commonly referred to as React, is a JavaScript library designed for building user interfaces. When using React.js, developers typically write their applications in plain JavaScript, often leveraging modern JavaScript features such as ES6 and ES7. One notable characteristic of React.js is that it doesn’t enforce strict typing by default. As a result, developers rely on runtime checks and tools like PropTypes for type validation and error detection.
React TypeScript: React TypeScript, on the other hand, involves the use of TypeScript, a statically typed superset of JavaScript, in React application development. With React TypeScript, developers write their React components using TypeScript’s syntax. This approach offers a significant advantage: static type checking during development. TypeScript empowers developers to define types and interfaces for props, state, and other data, which can catch type-related errors at compile-time rather than runtime. This leads to improved code quality, enhanced code predictability, and a reduction in runtime errors.
In summary, React.js is the JavaScript library for building user interfaces, while React TypeScript is the same library but integrated with TypeScript to provide enhanced type safety and development support. The choice between React.js and React TypeScript depends on project requirements, developer preferences, and the importance of static typing for a particular application. Both options are valid and widely used in the development of web applications and user interfaces.

Should I start React with TypeScript?

Starting a React project with TypeScript or JavaScript depends on various considerations.
Beginning with TypeScript: Starting with TypeScript can be advantageous when you prioritize strong type safety and improved development tooling. TypeScript’s static type checking helps catch errors at compile-time, leading to more robust and maintainable code. If you’re working on a sizable or complex project, TypeScript can be particularly beneficial in preventing bugs and making the codebase easier to manage. TypeScript also provides enhanced code documentation through type definitions, which can improve code readability and collaboration within your team.
Opting for JavaScript: Choosing JavaScript may be more suitable for smaller projects or when you’re working under tight deadlines. JavaScript is more lightweight and has a shorter learning curve, making it quicker to set up and get started. If your team lacks experience with TypeScript or if the project requirements don’t necessitate strong typing, JavaScript might be a pragmatic choice. Additionally, the JavaScript ecosystem boasts an extensive collection of libraries and resources, making it easier to find solutions and support for your project.

Joe PreviteJoe Previte
View Author

Joe Previte is a developer, a teacher and creator of Vim for VSCode. In his spare time, he enjoys creating videos for egghead, writing articles relating to coding and leading an online meditation study group.

ReactTypeScript
Share this article
Read Next
Get the freshest news and resources for developers, designers and digital creators in your inbox each week