How do I convert my React app from JavaScript to TypeScript?
Introduction
At some point in your work in React, you’ll likely inherit a project that was built in JavaScript and find that you want to start using TypeScript. I’m not going to go into the details of TypeScript vs JavaScript except that it lends itself to better type safety for React applications and has largely become the standard for many organizations that are working with React. We’ll focus on converting your React app from TypeScript to JavaScript. This article is for Create React App applications (those created using the command:
npx create-react-app <name_of_app>
to scaffold a project). In my specific case, I am converting a project for a free Material UI template from JavaScript to TypeScript (since the free version is all in JavaScript). This project was created using Create React App, and as such, the webpack config is maintained within the system rather than manually. More on that later.
Installing TypeScript and types packages
To get started, you need to install TypeScript and the type packages that go along with the libraries that you’re using. Because TypeScript requires type declarations, type packages typically exist for popular libraries and must be included so that TypeScript doesn’t throw errors for your library components. I used the following command to install TypeScript and the types for the major packages I’m using in my project. At a minimum, most React projects are likely going to need all of these types packages (not just my project).
npm install --save typescript @types/node @types/react @types/react-dom @types/jest
At this point, you should still be able to build and run your project without anything coming up different. Everything should still work the same at this point (and I did this is as a sanity check to make sure nothing broke with the new installations).
Adding the tsconfig.json file
Once you’ve installed everything, next you’ll need to create your tsconfig file. You can use the following command to get a tsconfig file with a list of all possible settings you can change:
npx tsc --init
This creates the config that is used by the TypeScript compiler to run and check your types and build your app. This file replaces the jsconfig.json file that is part of React apps that run using JavaScript only. Your application will not build if both a jsconfig.json and a tsconfig.json file are included in the project.
You will need to port over the settings from your old jsconfig into your new tsconfig file. In my case, my jsconfig.json file contained the following:
{
"compilerOptions": {
"jsx": "react-jsx",
"target": "esnext",
"module": "esnext",
"baseUrl": "src",
"moduleResolution": "node"
},
"include": ["src/**/*"],
"exclude": ["node_modules"]
}
To port your settings over to the new tsconfig, you’ll enable the options from your jsconfig with the same values. What I mean by this is that you’ll see in the new tsconfig, that all the options that you can possibly set for TypeScript are in the file, but most of them are commented out (but available so you can decide how you want to customize the Typescript compiler). Here is an example from inside my initialized file as an example.
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
To update your tsconfig, just find the same field as that is in the jsconfig file, and set it to the same value. For example, since my jsconfig contains:
"jsx": "react-jsx",
I needed to update my tsconfig by enabling that same setting with that same value as so:
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"jsx": "react-jsx",
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
In the same pattern, go through and update the rest of the tsconfig settings with what you have in your jsconfig. Here is a sample from my tsconfig.
{
"include": ["src/**/*"],
"exclude": ["node_modules"],
"compilerOptions": {
// ...settings
"target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
// "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
"jsx": "react-jsx" /* Specify what JSX code is generated. */,
/* Modules */
"module": "commonjs" /* Specify what module code is generated. */,
// "rootDir": "./", /* Specify the root folder within your source files. */
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
"baseUrl": "src" /* Specify the base directory to resolve non-relative module names. */,
Now my tsconfig has all the settings from the jsconfig, and so the project at this point should be able to build with the TypeScript compiler and still allow all the JavaScript components that haven’t been converted to TypeScript to continue working.
You should still be able to build and run your React app with the same functionality as before. The only difference now is that you can start using TypeScript.
Webpack Updates
You shouldn’t need to make any changes to WebPack since Create React App manages this process for you. You can eject your app so that you have access to the files to make your configurations, but in general, if you use create-react-app, you typically stick with the config management they give you. If you don’t like those limitations, then it might be a good idea to build from scratch so that you have full control over your configs. Or maybe you’re clear on all the settings given by create-react-app and so want to manually change them. Regardless of your feelings on managing your own webpack config files, you don’t need to eject and modify the webpack config for adding TypeScript to an app created with Create React App.
Converting your app to TypeScript
Now that you have TypeScript installed and configured, pick a really simple JavaScript component to convert to TypeScript. You will need to specifically replace the current file extension with the .tsx extension for any component you’re converting to TypeScript (i.e.: MyComponent.js –> MyComponent.tsx), as this is the way the TypeScript compiler determines the file is for a React component written in TypeScript. This will lay the pattern for converting the rest of your application to TypeScript and helps you test to verify that everything is set up right for being able to write TypeScript in your application.
Process for converting the rest of your application
Over the process of converting to TypeScript, start with the lowest level and easiest components, and work your way up. It takes some time to resolve all the type errors. I’ve seen some recommendations to start with a lax tsconfig file, then make it more strict as you go on. The hard thing about that is that you end up needing to go back to reconvert everything to stricter levels of TypeScript. My personal opinion is that you set up your tsconfig to what your needs are (I’ve found that the default has done well enough for the projects I’ve worked on) and build each of your TypeScript components according to that standard.
Installing missing types packages
You might find that while you’re working in your project that there are other packages that you’re using that are causing the TypeScript compiler to complain because you’ve not included the types for those packages. As I mentioned above, most major libraries have a types package associated with them so that they can be used with TypeScript. If you find that you are getting types errors for any third party code, then you’ll need to install the types to go with that library. For example, for the MomentJS package:
npm install --save @types/momentjs
Manually updating types definitions
For some cases, such as for obscure npm packages, or, rarely, for established packages, you may need to update or create a *.d.ts file for specifying the types that you’re using in your application. These types files are used by the TypeScript compiler to determine the type for the given item that you’re using. For example, many of the types used by Material UI’s theme are contained in the createPalette.d.ts file.
export interface PaletteColor {
light: string;
main: string;
dark: string;
contrastText: string;
}
In my case, I was working on converting a React component from JavaScript to TypeScript in a Material UI template and most of the conversions were pretty straightforward, but I ran into an issue with the following code:
useEffect(() => {
setOptions((prevState) => ({
...prevState,
// typescript compiler complaining about theme.palette.primary[700] below
colors: [theme.palette.primary.main, theme.palette.primary[700]],
// ...
});
});
On this line, we’re using JavaScript’s syntax to access an object field using array notation which accepts a string or number to access that specific field. TypeScript allows you to do the same, but requires the types to be defined in order to do so. While using TypeScript, I’m not a fan of this notation, so I wanted to update the types to include a named field that I could access. The custom theme for this template had “primary” (from the line above — theme.palette.primary) defined as follows:
primary: {
lighter: blue[0],
100: blue[1],
200: blue[2],
light: blue[3],
400: blue[4],
main: blue[5],
dark: blue[6],
700: blue[7],
darker: blue[8],
900: blue[9],
contrastText
},
For this object, the fields are either a string or a number, and I really didn’t want to access the color via a number. I updated the file to include a second definition (I hadn’t yet found and replaced all other usages of 700, so I wasn’t going to completely replace it at this point) for “blue[7]” that I could assign a field name that I believed was more representative of how it is being used for throughout the project.
primary: {
lighter: blue[0],
100: blue[1],
200: blue[2],
light: blue[3],
400: blue[4],
main: blue[5],
dark: blue[6],
700: blue[7],
color700: blue[7], // My new field
darker: blue[8],
900: blue[9],
contrastText
},
I then updated the createPalette.d.ts file to include this new field:
export interface PaletteColor {
light: string;
main: string;
dark: string;
color700: string; // new field added to type definition
contrastText: string;
}
This type interface is used across all colors, so I left the field name more generic rather than creating specific types for each color. While this isn’t necessarily the cleanest approach, I wanted to include it here as a demonstration on how to update *.d.ts files when needed. These changes allowed me to update the above problematic code to the following:
useEffect(() => {
setOptions((prevState) => ({
...prevState,
// compiler is now happy, and I find this more readable
colors: [theme.palette.primary.main, theme.palette.primary.color700],
// ...
});
});
This change fixed the TypeScript error, and I was able to build and run my app, and now the component is converted from JavaScript to TypeScript.
Conclusion
Ultimately, my opinion is that if you are questioning whether to use TypeScript of JavaScript in your React app, choose TypeScript, and get it set up with TypeScript right out the gate. It saves the pain of revisiting old code to convert later and the other idiosyncrasies that come up with having JavaScript and TypeScript live together. But if you find that you’ve inherited a JavaScript application and you want to convert to TypeScript, then this is the approach that I’ve liked found to be the easiest.
npx create-react-app my-app --typescript