Zephyrnet Logo

React with TypeScript: Best Practices

Date:

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 official TypeScript Handbook 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 follow best practices in your projects by using this article as a reference.

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 "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 "experimentalDecorators": true, // Enables experimental support for ES decorators "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 his blog, you can read more about the “why” behind these rules and configurations.

VSCode 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 VSCode. 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.

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 other best practices 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.

Source: https://www.sitepoint.com/react-with-typescript-best-practices/?utm_source=rss

spot_img

Latest Intelligence

spot_img

Chat with us

Hi there! How can I help you?