Tips for migrating a React app to Typescript
Tags: react, typescript, javascript
Published: 2020-03-19 13:47:17

So, I've been working on a company project for almost half a year now. After feature development was finished and work on it slowed down, I decided I wanted to take the next step, which I did not dare take 6 months back, and write (or in this case "migrate") the project to Typescript. 

These are all the things I learned in the process and, in retrospect, I wish I knew when I was starting the migration.

## Disclaimer 📣

* This post assumes you are a beginner with TS but its purpose is not to teach you TS. Its purpose is to give some advice to the next person who wants to try something similar and make their lives easier.

* This post assumes that you are working on a CRA project and most of the setup and configuration is already been taken care for you. However most of the advice could be applied on any React project.

* This post is based on *my* experience with migrating a moderately big codebase to TS. 

## Before you begin ⚙️

### Set `allowJs` in your tsconfig to `true`

That's all. This will ensure that you are not drowning in red the moment you enable the type checker and will allow you to migrate gradually; letting you keep your existing `.js` files, until their time comes.

### Create a `global.d.ts` file in your `src` directory

This will come in handy, since you most likely will want to [augment or extend](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) the types of existing libraries you are using. 

### Create a `custom.d.ts` file in your `src` directory

You will need this, in order to be able to [import files that are not code modules](https://webpack.js.org/guides/typescript/#importing-other-assets), such as image files, `.json` files, etc. To help you get started, just add this inside your `custom.d.ts` file:

```typescript
declare module '*.svg' {
  import React = require('react');
  export const ReactComponent: React.FunctionComponent<React.SVGProps<SVGSVGElement>>;
  const src: string;
  export default src;
}

declare module '*.json' {
  const content: object;
  export default content;
}
```

Et voila, now you can import `.svg` and `.json` files inside your code modules without TS throwing errors at you.

### Should you want to enable `noImplicitAny` and `no-explicit-any` do it before you start

`noImplicitAny` is a [compiler option](https://www.typescriptlang.org/docs/handbook/compiler-options.html) which will raise errors on expressions and declarations with an implied `any` type.

`no-explicit-any` is an [`eslint` rule](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-explicit-any.md) that doesn't allow you to define anything as `any` type. 

If you do enable these options, the [`unknown`](https://www.google.com/search?q=typescript+unknown+vs+any&oq=typescript+unknown) type will be your friend. 

These options should ideally be enabled from the get go. No matter what, **do not enable them during the migration process**. I made that mistake and ended up suddenly having to spend lots and lots of time resolving quite an amount of errors that I "inflicted" upon myself. Can be quite disheartening while you are still in the middle of the migration.

### Setup your `eslint` config for use with TS

I happened to have an issue with `eslint`'s default `no-unused-vars` rule and TS. Turns out there is a specific [`no-unused-vars`](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md) rule for TS. Enable this and [disable the default one](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-unused-vars.md#options)

### Settle on how you will be defining your types

Will you be using interfaces or types? Will you be writing your types inside the code file or as a separate file? I advise that you settle on these details before you start because you don't want to change you mind halfway there and have to retrofix all the files you have already worked on. 

My advice is use types unless an interface is absolutely necessary and keep your type definitions separate from the component files themselves. 

The system I applied was the following:

* If the file is a React component file, create a separate `types.d.ts` file in the same directory and put all the type definitions there. Except for the `Props` type definition; I keep that in the component file itself, since it is handy to have the props definition readily available.
* If the file is not a React component type, declarations go along with the code. No separate `types.d.ts` file. That is unless the types are so many that make the code file really messy, then they are taken out in their own file.

### Read the documentation

Seriously. At least read some of the important parts, such as:

* [Basic types](https://www.typescriptlang.org/docs/handbook/basic-types.html)
* [Generics](https://www.typescriptlang.org/docs/handbook/generics.html)
* [Advanced types](https://www.typescriptlang.org/docs/handbook/advanced-types.html) - Especially how [intersections](https://www.typescriptlang.org/docs/handbook/advanced-types.html#intersection-types) and [unions](https://www.typescriptlang.org/docs/handbook/advanced-types.html#union-types) work, because they do not map exactly 1:1 with the mathematical definition of unions and intersections
* [Type compatibility](https://www.typescriptlang.org/docs/handbook/type-compatibility.html)
* [Utility types](https://www.typescriptlang.org/docs/handbook/utility-types.html) - Those are *very* handy, give them a look to, at least, know of their existence.
* Release notes - I have found that sometimes things mentioned in the release notes of a new version are not mentioned in the docs. For example this [very handy assertion function functionality](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) which was added in version 3.7 and AFAIK is not mentioned anywhere in the docs.

### Prepare to start being more thorough when you code

Typescript will eventually lead you to be more thorough with some assumptions you make while coding. Assumptions for which, in your mind, know to be correct for your code, are not enough for TS. Typescript will always be asking to validate those assumptions by writing some more code, mostly in the form of

```js
if (notCondition) { 
  throw new Error()
}

// Otherwise all is well
```

You'll get used to it, and eventually it will come to you naturally.

### Get ready to be wrong

Accept the fact that 99.999% of times the TS compiler will be right and you will be wrong 😅

## On with the actual migration 🚀

### Start small

When you make the transition from JS to TS you want to start small. See that directory with your utility functions? They are by far the simplest things you can start migrating over to TS. 

Once you migrate a simple function see where this function is used, what other files import it (the compiler will probably let you know, by throwing some errors at you at this point). 

Proceed with migrating those files, and repeat the process, essentially climbing up (down?) the dependency tree. If you reach a file that is far too complex for this stage of the migration don't be afraid to put a `@ts-nocheck` directive at the first line of it, and migrate it at a later time.

### Do not be afraid to do type assertions

Type assertions in TS are like type casting in other languages. In essence you are telling the TS compiler that you know better, and a specific variable, even though it *could* possibly be of many types, cannot be anything else other than a specific type in this case. Sometimes you *do* in fact know better. But only sometimes 😅

I have found this to be useful when working with React hooks that provide some [context](https://reactjs.org/docs/hooks-reference.html#usecontext) value. Context values start with a "default" value when you initialise the `Context`, which may not always be compatible with the actual value passed to the provider.  More on that in [this](https://dev.to/mitchkarajohn/working-with-react-context-providers-in-typescript-1fk0) post.

Custom [type guards](https://www.typescriptlang.org/docs/handbook/advanced-types.html#type-guards-and-differentiating-types) and [assertion functions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions) are also very useful in helping you help TS make the correct type assertions

### Do not use the provided `FC` type for React components

I highly encourage you to not define React components like this

```ts
const Comp = FC<Props>(props) => { .... }
```

I found its usage to be more of a bother than anything, since it makes some rather arbitrary assumptions about your components (like always having `children` props and not working very well with `PropTypes`) and in general takes away some of the control you have over defining your components the way you want. Use

```ts
function Comp(props: Props) { .... }
``` 

instead. TS is smart enough to infer the return type itself, and for all intents and purposes this is a valid React component, which you can use in any case where a React component is expected by the compiler. 

### Keep your `PropTypes` 

While TS is very useful in making sure you are not making any type related mistakes during development time, `PropTypes` are very useful in letting you know of type related mistakes during runtime. How can you have type related errors during runtime, if you are using TS for development, you ask? Consider this scenario: 

You have defined your API response type in TS as bringing back a field which is supposed to be a `number`. You have also defined your relevant `PropTypes` field as such. All is well. 

Now, imagine if your API returns a `string` instead of a `number` in that field. Had you removed the `PropTypes` you would never realise the error early, until the app crashed at some point. With `PropTypes` you will get a very useful warning in the browser console if any such mismatch ever occurs.

If you work on an ejected CRA project, or otherwise you have access to the `babelrc` file, know that a [plugin](https://www.npmjs.com/package/babel-plugin-typescript-to-proptypes) exists that can automatically convert your TS types to `PropTypes`, so that you don't have to manually update both.

### Export all your types

Even if you don't end up importing all of them in other files, make a habit of exporting them since you never know when you may need a type which is defined in another module.

### Do not be afraid to use generics

TS [generics](https://www.typescriptlang.org/docs/handbook/generics.html) can be really helpful and you can have generic React components as well. For example

```ts
// Comp.tsx
type Props<T> = {
  result: T;
}

function Comp<T>(props: Props<T>) {
// T can also be used inside the function body too, if needed
}


// OtherComp.tsx
type SomeType = ...

function OtherComp() {
  return (
    <Comp<SomeType> someProp={...}>
      <SomeContent />
    </Comp>
  )
}
```

In my experience, if a React component is an abstraction over some thing, then it's safe to assume that this component's type definition will be a generic.

### Read the type definitions of libraries you use

When in doubt always read the type definitions for the libraries you use. This will also help you on how you should define your own component types for usage with components from external libraries.

### You don't always have to define the return types

TS is (mostly) smart enough that it will correctly infer the type of the return values of functions. Personally, I like to define return values too, but that's because I have some kind of OCD 😅

Do note that there may be times that you'll have to strictly define the return value for things to work correctly.

### Make type definitions for your API responses

They will help you immensely, since it's more than likely that you will be using server provided data in parts of your application

### Learn to read TS compiler errors

TS errors can be intimidating, however there is a "cheat" in how to read them. Always read the first and the last few lines of the error message. If you still don't make sense do read the whole error, but usually just reading those lines will give you the information you need to make sense of the error.

## Random Typescript tip ✅

### Do `(TypeA | TypeB)[]` don't `TypeA[] | TypeB[]`

When you have an array whose type may be either an array of `TypeA` or an array of `TypeB` declare it as

```ts
const arr: (TypeA | TypeB)[]
```

instead of 

```ts
const arr: TypeA[] | TypeB[]
```

There is a [subtle difference](https://stackoverflow.com/a/49511416/4651083) between those 2 declarations and the second one will lead to errors if you try to `.map()` over the `arr`

## Closing words

The fact that you decided to *migrate* your app instead of writing it in TS from the start, may conceal the merits of having used TS at the beginning, since your whole code base is already laid out.

**However** you *will* see the merits once you continue development and now all your new code will have to abide by the TS rules.

Remember, your code is now quite a lot more thorough with handling error cases. 

This, combined with some proper tests, will ensure that your code is as robust as it can be.

Thank you for reading! 🎉