Telerik blogs

Considering adopting TypeScript for your React app? Learn what to watch for before you make the switch.

TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at scale. It has gained wide adoption and I’ve seen many React developers switch to using TypeScript for their React applications.

While there’s wide adoption of TypeScript, there are still a lot of teams that don’t use TypeScript but are interested in adopting TypeScript for their React projects. These are some of the concerns or questions that these teams or developers wonder about:

  • How to define types for React components, props and state
  • How to use React hooks and event handlers in TypeScript
  • Setting up a TypeScript project
  • Migrating existing applications to TypeScript

I will address the first three of these questions in this post, and in a later post, we will see how to migrate a React application to TypeScript.

Prerequisites

For a better understanding of this post, you should be familiar with React and have basic knowledge of TypeScript. I will show you how to create a new project, define components, and work with hooks and functions in a React application.

Let’s get started! 🚀

Setting Up a TypeScript Project

The first question that comes up when I discuss TypeScript with a React dev is: “How do I create a React TypeScript Project?” You most likely use a tool to create a new React project, for example Create React App (CRA), which allows you to set up a modern web app by running the command npx create-react-app react-app. Frameworks like Next.js, Remix and Gatsby have their own CLI that enables you to set up a TypeScript-base app.

For CRA, you use the command npx create-react-app react-app --template typescript to create a TypeScript app. For Next.js it’s npx create-next-app --typescript. Whichever framework/tool you choose to use, check the documentation for how to use TypeScript.

Using these CLIs, you don’t have to configure or specify any TypeScript configuration—they’re automatically generated. You can tweak the generated tsconfig.json file to suit your needs. You can add some community recommended config via npm, for example using @tsconfig/esm. You can see more info about the different tsconfig extensions at github.com/tsconfig/bases. I use the Node 16 and Node 17 configuration when building Node.js apps, but using a tool like create-next-app I usually don’t need additional configuration.

Using ESLint

You can add TypeScript support to ESLint using the packages @typescript-eslint/parser and @typescript-eslint/eslint-plugin. The former is used to parse TypeScript code to a format that is understandable by ESLint, while the latter provides TypeScript-specific linting rules. Add them to your project using the command below:

npm install -D @typescript-eslint/parser @typescript-eslint/eslint-plugin

Then add the following config to your .eslintrc.js

module.exports = {
  parser: "@typescript-eslint/parser",
  parserOptions: {
    ecmaVersion: "latest", // Allows the use of modern ECMAScript features
    sourceType: "module", // Allows for the use of imports
  },
  extends: ["plugin:@typescript-eslint/recommended"], // Uses the linting rules from @typescript-eslint/eslint-plugin
  env: {
    node: true, // Enable Node.js global variables
  },
};

Check the documentation at github.com/typescript-eslint/typescript-eslint to learn more about TypeScript + ESLint.

JSX & TypeScript

You can name your files with the .tsx extension when working with TypeScript. You don’t need to configure anything extra if you used a tool to bootstrap the project. Otherwise, TypeScript has a jsx configuration option that controls how JSX constructs are emitted in JavaScript files. This only affects the output of JS files that end in .tsx.

Learn more about JSX under the hood.

Function Component

Function components can be written as regular functions that accept arguments and return a React element. For example:

export const HelloWorld = () => <h1>Hello World!</h1>;

In this example, the return type is inferred by TypeScript. You can choose to specify the return type so that an error is raised if you accidentally return a different type. For example:

export const HelloWorld = (): JSX.Element => <h1>Hello World!</h1>;

Typing Props

You can define the type for the function argument either inline or as a separate type declaration.

Here’s an example with the prop type defined inline:

export const HelloWorld = ({ message }: { message?: string }) => (
  <h1>Hello {message ?? "World"}!</h1>
);

You can declare a type using either the type alias or as an interface. My personal preference is to use interface until I have a need to use the type alias. But I’ve worked with teams that prefer to use the type alias in all cases and I go with their approach. You can read more about the differences in the TypeScript docs, and explore an example in the TypeScript playground.

Children Props

If the component accepts other React components as props, you can include the children property and set its type to React.ReactNode.

interface HelloProps {
  message: string;
  children?: React.ReactNode;
}

export const HelloWorld = ({ message, children }: HelloProps) => (
  <>
    <h1>Hello {message ?? "World"}!</h1>
    <div>{children}</div>
  </>
);

What About CSS Styles?

You may ask, “How would I specify the type of the style attribute of an element?” You can do that using the React.CSSProperties type.

Here’s an example:

interface HelloProps {
  message: string;
  style?: React.CSSProperties;
}

export const HelloWorld = ({ message, style }: HelloProps) => (
  <>
    <h1 style={style}>Hello {message ?? "World"}!</h1>
  </>
);

Hooks

React hooks have been available since version 16.8, and many would encourage you to use functional components with hooks, rather than class components. I will stick to functional components in this post. Check out this cheatsheet if you want to know about using TypeScript with React class components.

useState

I won’t go into details about every React hook available. I’ll rather pick a few and show you how to work with hooks. Let’s start with the useState hook.

You can call useState hook and specify the type for the state as follows:

const [message, setState] = useState<string>("hello world");

By specifying the type, only string values will be allowed when calling the hook or using the setState function. However, type inference works really well in this case. You can rewrite the example above to use inference using the code below:

const [message, setState] = useState("hello world");
// inferred type for `message`: string
// setState only accepts string a value

Sometimes you want to call useState without any argument (or use a null value), and later on update the state with the actual value. In this case, you would need to explicitly specify the type using a union type. For example:

const [message, setState] = useState<string | undefined>();
// `message` type: string | undefined
// setState only accepts string a value or undefined

When you use type inference and you need the inferred type, you can use the typeof operator to get the type. For example:

const [state, setState] = React.useState({
  id: 104,
  name: "Joe Lars",
  passportNumber: "A093JK23",
}); // the inferred type is {id: number, name: string, passportNumber: string}

const sendMessage = (user: typeof state) => {
  findPassport(user.passportNumber); // a function using `user`
};

useContext / useMemo / useCallback

Other hooks also have type inference support. Take useContext for example—it can infer the type based on the object that is passed in as an argument.

type Color = "grey" | "black" | "pink";
const ColorContext = createContext<Color>("pink");

const Header = () => {
  const colour = useContext(ColorContext);
  return <div style={{ colour }}>The legend of Kora</div>;
};

useMemo and useCallback also have their types inferred from the values that they return.

Events

The types for events are inferred through contextual typing if the event handler is defined inline.

<button
  onClick={(e) => {
    // `e` will be correctly typed automatically!
  }}
/>

If you define the handler function separately, then you will need to specify the type. The code below is an example of using a text input.

const Input = () => {
  const [state, setState] = useState("");

  const handleChange = (e: ChangeEvent<HTMLInputElement>) => {
    setState(e.target.value);
  };

  const handlePaste = (e: ClipboardEvent<HTMLInputElement>) => {
    e.preventDefault();
  };

  return (
    <input
      type="text"
      value={state}
      onChange={handleChange}
      onPaste={handlePaste}
    />
  );
};

The code snippet has two event handler functions handleChange and handlePaste. The event object e for handleChange uses the ChangeEvent generic type, while handlePaste uses ClipboardEvent generic type. Every event has a distinct type. The cut, copy and paste events uses the ClipboardEvent. You can look up the list of event types, and more about events in the React TypeScript Cheatsheet.

Conclusion

In this post, I showed you how to set up a new React project using TypeScript, and how to work with functional components, hooks and event handlers using TypeScript. Stay tuned for a later post walking you through migrating a React project to TypeScript.


Peter Mbanugo
About the Author

Peter Mbanugo

Peter is a software consultant, technical trainer and OSS contributor/maintainer with excellent interpersonal and motivational abilities to develop collaborative relationships among high-functioning teams. He focuses on cloud-native architectures, serverless, continuous deployment/delivery, and developer experience. You can follow him on Twitter.

Related Posts

Comments

Comments are disabled in preview mode.