DEV Community

Cover image for Tips for migrating a React app to Typescript
Dimitris Karagiannis
Dimitris Karagiannis

Posted on • Edited on

Tips for migrating a React app to Typescript

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 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, such as image files, .json files, etc. To help you get started, just add this inside your custom.d.ts file:

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;
}
Enter fullscreen mode Exit fullscreen mode

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 which will raise errors on expressions and declarations with an implied any type.

no-explicit-any is an eslint rule that doesn't allow you to define anything as any type.

If you do enable these options, the 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 rule for TS. Enable this and disable the default one

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:

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

if (notCondition) { 
  throw new Error()
}

// Otherwise all is well
Enter fullscreen mode Exit fullscreen mode

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

Custom type guards and 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

const Comp = FC<Props>(props) => { .... }
Enter fullscreen mode Exit fullscreen mode

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

function Comp(props: Props) { .... }
Enter fullscreen mode Exit fullscreen mode

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 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 can be really helpful and you can have generic React components as well. For example

// 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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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

const arr: (TypeA | TypeB)[]
Enter fullscreen mode Exit fullscreen mode

instead of

const arr: TypeA[] | TypeB[]
Enter fullscreen mode Exit fullscreen mode

There is a subtle difference 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! 🎉

Top comments (0)