Incrementally adopting TypeScript in a create-react-app project
Tags: typescript, create, react, app
Published: 2022-01-11 16:13:01

---
title: Incrementally adopting TypeScript in a create-react-app project
published: true
description: Incrementally adopting TypeScript in a create-react-app project
tags: typescript, create, react, app
cover_image: https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3vi96xo4ny6bdus8scz1.png
---

You can gradually adopt TypeScript in your create-react-app project. You can continue using your existing Javascript files and add as many new TypeScript files as you need. By starting small and incrementally converting JS files to TypeScript files, you can prevent derailing feature work by avoiding a complete rewrite.

Incrementally adopting TypeScript in a create-react-app project can be valuable, especially if you don't want to do a full-fledged migration before you fully learn TypeScript or become more proficient with it. 

For this tutorial, the app we'll be converting to TypeScript is a counter app built with `redux-toolkit`, if you're not familiar with [redux](https://redux.js.org/), [redux-toolkit](https://redux-toolkit.js.org/) or [TypeScript](https://www.typescriptlang.org/), I highly suggest you take a look at their docs before doing this tutorial as I assume you have some basic understanding of all of them. 

Before you start please make sure you don't have `create-react-app` globally installed since they no longer support the global installation of Create React App.

```bash
Please remove any global installs with one of the following commands:
- npm uninstall -g create-react-app
- yarn global remove create-react-app
```

First, let's bootstrap a React app with [Create React App](https://github.com/facebook/create-react-app), using the [Redux](https://redux.js.org/) and [Redux Toolkit](https://redux-toolkit.js.org/) template.

```zsh
npx create-react-app refactoring-create-react-app-to-typescript --template redux 
```

Here is a visual representation of the project's directory and file structure. 

```md
📦 refactoring-create-react-app-to-typescript
 ┣ 📂 node_modules
 ┣ 📂 public
 ┣ 📂 src
 ┃ ┣ 📂 app
 ┃ ┃ ┗ 📜 store.js
 ┃ ┣ 📂 features
 ┃ ┃ ┗ 📂 counter
 ┃ ┃ ┃ ┣ 📜 Counter.module.css
 ┃ ┃ ┃ ┣ 📜 Counter.js
 ┃ ┃ ┃ ┣ 📜 counterAPI.js
 ┃ ┃ ┃ ┣ 📜 counterSlice.spec.js
 ┃ ┃ ┃ ┗ 📜 counterSlice.js
 ┃ ┣ 📜 App.css
 ┃ ┣ 📜 App.test.js
 ┃ ┣ 📜 App.js
 ┃ ┣ 📜 index.css
 ┃ ┣ 📜 index.js
 ┃ ┣ 📜 logo.svg
 ┃ ┣ 📜 serviceWorker.js
 ┃ ┗ 📜 setupTests.js
 ┣ 📜 .gitignore
 ┣ 📜 package-lock.json
 ┣ 📜 package.json
 ┗ 📜 README.md
```

Also, feel free to take a look at the final version of the project [here](https://github.com/mannycolon/refactoring-create-react-app-to-typescript), if you want to see the original Javascript version go [here](https://github.com/mannycolon/refactoring-create-react-app-to-typescript/tree/javascript).

## Adding TypeScript to create-react-app project

> Note: this feature is available with react-scripts@2.1.0 and higher.

[TypeScript](https://www.typescriptlang.org/) is a typed superset of JavaScript that compiles to plain JavaScript.

### Installation

To add [TypeScript](https://www.typescriptlang.org/) to an existing Create React App project, first install it:

```zsh
npm install --save typescript @types/node @types/react @types/react-dom @types/jest

# or

yarn add typescript @types/node @types/react @types/react-dom @types/jest
```

Now, let's start by renaming the index and App files to be a TypeScript file (e.g. `src/index.js` to `src/index.tsx` and `App.js` to `App.tsx`) and create a `tsconfig.json` file in the root folder.

> Note: For React component files (JSX) we'll use `.tsx` to maintain [JSX support](https://facebook.github.io/jsx/) and for non React files we'll use the `.ts` file extension. However, if you want you could still use `.ts` file extension for React components without any problem. 

Create `tsconfig.json` with the following content:

```json
{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "react-jsx"
  },
  "include": [
    "src"
  ]
}
```

Next, restart your development server!

```zsh
npm start

# or

yarn start
```

When you compile `src/App.tsx`, you will see the following error:

![SVG type error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3qjx041rjzk9c519im75.png)

## Solution with custom.d.ts

At the root of your project create `custom.d.ts` with the following content:

```ts
declare module '*.svg' {
  const content: string;
  export default content;
}
```

Here we declare a new module for SVGs by specifying any import that ends in `.svg` and defining the module's content as string. By defining the type as string we are more explicit about it being a URL. The same concept applies to other assets including CSS, SCSS, JSON, and more.

See more in Webpack's documentation on [Importing Other Assets](https://webpack.js.org/guides/typescript/#importing-other-assets).

Then, add `custom.d.ts` to `tsconfig.json`.

```json
{
  ...,
  "include": ["src", "custom.d.ts"]
}
```

Restart your development server.

```zsh
npm start

# or

yarn start
```

You should have no errors and the app should work as expected. We have converted two files (Index.js -> index.tsx and App.js -> App.tsx) to TypeScript without losing any app functionality. Thus, we've gained type checking in our two converted files. 

Now, we can incrementally adopt TypeScript in our project one file at a time. Let's do exactly that, starting with `Counter.js`. Change `Counter.js` to `Counter.tsx`.

Restart the app, `npm start` or `yarn start`.

It will complain that it cannot find module `./Counter.module.css` or its corresponding type declarations.

![Missing type declarations for CSS modules](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/p17chyoj1hrhp1sweqg8.png)
 
We can fix it by adding a type declaration for `*.module.css` to the end of `custom.d.ts`. So, our `custom.d.ts`  file should look as follow:

`custom.d.ts`

```ts
declare module '*.svg' {
  const content: string;
  export default content;
}

declare module '*.module.css';
```

Alternatively, you could also use [typescript-plugin-css-modules](https://www.npmjs.com/package/typescript-plugin-css-modules) to address the CSS modules error but adding a type declaration is good enough in this case. 

The next error/warning is related to `incrementAsync`.

![incrementAsync type error](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0bb4s3npx0qfg2mrk65o.png)

However, before we fix the second error in `counterSlice.tsx`, we must change `src/app/store.js` to `src/app/store.ts` then define Root State and Dispatch Types by inferring these types from the store itself which means that they correctly update as you add more state slices or modify the middleware setting. Read more about using TypeScript with Redux in their [TypeScript docs](https://redux.js.org/usage/usage-with-typescript).

`src/app/store.ts` should look as follows.

```typescript
import { configureStore } from '@reduxjs/toolkit';
import counterReducer from '../features/counter/counterSlice';

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});

// Infer the `RootState` and `AppDispatch` types from the store itself
export type RootState = ReturnType<typeof store.getState>;
// Inferred type: {posts: PostsState, comments: CommentsState, users: UsersState}
export type AppDispatch = typeof store.dispatch;
```

Okay, now that we have defined Root State and Dispatch Types let's convert `counterSlice` to TypeScript.

`src/features/counter/counterSlice.js` -> `src/features/counter/counterSlice.ts`

In `counterSlice.ts` the first error is that the type of the argument for payload creation callback is missing. For basic usage, this is the only type you need to provide for `createAsyncThunk`. We should also ensure that the return value of the callback is typed correctly.

The `incrementAsync` function should look like this:

```ts
// The function below is called a thunk and allows us to perform async logic. It
// can be dispatched like a regular action: `dispatch(incrementAsync(10))`. This
// will call the thunk with the `dispatch` function as the first argument. Async
// code can then be executed and other actions can be dispatched. Thunks are
// typically used to make async requests.
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  // Declare the type your function argument here:
  async (amount: number) => {// HERE
    const response = await fetchCount(amount);
    // The value we return becomes the `fulfilled` action payload
    return response.data;
  }
);
```

We added a type (`number`) to the argument called `amount` in the callback function passed to `createAsyncThunk` as the second argument.  

Before we go on with the other type errors, we must address the error with the response value returned from the `fetchCount` function inside the function callback passed to `createAsyncThunk` in `incrementAsync`. In order to fix it we must first fix it at the root of the problem, inside `counterAPI.js`. 

Thus, first convert `counterAPI.js` to `counterAPI.ts`.

```ts
type CountType = {
  data: number;
};

// A mock function to mimic making an async request for data
export function fetchCount(amount: number = 1) {
  return new Promise<CountType>((resolve) =>
    setTimeout(() => resolve({ data: amount }), 500)
  );
}
```

In this Promise, I have used the promise constructor to take in CountType as the generic type for the Promise’s resolve value.

Now, let's go back to `counterSlice.ts` and the next error is that the `selectCount` selector is missing a type for its argument. So, let's import the types we just created in `store.ts`. 

Import `RootState` and `AppDispatch` types:

```ts
import type { RootState, AppDispatch } from '../../app/store'
```

Use `RootState` as a type for `selectCount`'s argument (state) 

`selectCount`

```ts
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state: RootState) => state.counter.value)`
export const selectCount = (state: RootState) => state.counter.value;
```

`incrementIfOdd`

```ts
// We can also write thunks by hand, which may contain both sync and async logic.
// Here's an example of conditionally dispatching actions based on the current state.
export const incrementIfOdd =
  (amount: number) => (dispatch: AppDispatch, getState: () => RootState) => {
    const currentValue = selectCount(getState());
    if (currentValue % 2 === 1) {
      dispatch(incrementByAmount(amount));
    }
  };
```

Okay, we should have zero type errors or warnings now. We've converted the following files to TypeScript:

```bash
 src/app/store.ts
 src/features/counter/Counter.tsx
 src/features/counter/counterSlice.ts
 src/features/counter/counterAPI.ts
```

Finally, let's convert our test files:

Change `App.test.js` to `App.test.tsx` and `counterSlice.spec.js` to `counterSlice.spec.ts` 

Run your tests: 

```bash
npm test
```

or 

```bash
yarn test
```

All tests should pass, however, you may encounter the following problem:

```bash
"Property 'toBeInTheDocument' does not exist on type 'Matchers<any>'."
```

To fix it, you can try adding the following to `tsconfig.json`: 

```json
...,
  "exclude": [
    "**/*.test.ts"
  ]
```

All tests should pass now:

![Passing unit tests](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kfqesa6eyyctgacqqa6w.png)
 
Feel free to check out my [repo](https://github.com/mannycolon/refactoring-create-react-app-to-typescript) with the final version of this app. 

Thanks for following along, happy coding!