loading...
Cover image for Converting a Create-React-App using Craco to TypeScript

Converting a Create-React-App using Craco to TypeScript

loribbaum profile image Lori Baumgartner ・5 min read

My dev team recently had an internal conversation that went like this:

Coworker1: what is everyone’s appetite to exploring typescript as a standard on new stuff we do/create in the frontend?
Me: YASSSSS
Coworker2: I am on the fence on that.

After more conversation and consideration of the pros, cons, and challenges of migration and adoption into a living, breathing app, we decided to move forward with adding TypeScript to our 2-year-old app. I volunteered to lead the project since I was the only one on the team with on-the-job TS experience 😬.

What you should expect from this post:

  • The tech stack I started with
  • What it took for me to convert one file to TypeScript, ensure nothing was broken, and ensure linting worked the app still ran
  • Some of my favorite TypeScript "getting started" resources

The Tech Stack

Our app is on react-scripts@3.0.0 version for Create React App. Thankfully this meant TypeScript was already supported by the app.

Adding TypeScript

The easy parts

Well, if you use create-react-app this might be all you need:

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

While that command doesn't perform all the magic we need, it did set us on the right path. Adding this meant that react-scripts now knew we were using TypeScript. So the next time I ran the yarn start command to fire up the server, the jsconfig.json was removed and the server helpfully said something along the lines of "It looks like you're using TypeScript - we've made you a tsconfig".

The hard parts

Okay, so as easy as it was to make my app compatible with TS, it was not that easy to get it configured to work with my app. Here are just a few questions I ran into:

  • How do I get my app path aliases to still work?
  import Component from 'components/Component' // this should still work
  import Component from 'src/shared/components/Component' // don't make me do this
  • I only want to convert one file at a time - how can I import .tsx files inside .js files without needing to specify the file extension?
  • We had a lot of linting warnings that popped up as soon as I added a local .eslintrc.js. I don't blame TS for this, but you might run into a similarly frustrating cycle of having to resolve a lot of linting errors then seeing more, then fixing more, etc.

So what actually changed?

The final PR ended up having an 8-file diff. But my first attempt had a 73-file diff. Why is that, you wonder? Well, I totally dove into the rabbit hole of trying to fix one thing which led me to feeling as though I had to upgrade a dependency to be compatible with TypeScript which then meant other dependencies needed to be upgraded. There might have also been some things that broke when I upgraded dependencies - I'm looking at you react-scripts.

Here's the list of my final files I needed to make TypeScript happen:

  • Create /frontend/.eslintrc.js
  • Delete the jsconfig.json that create-react-app used
  • Add the tsconfig.json
  • yarn.lock changes
  • package.json changes with new dependencies
  • A new /react-app-env.d.ts file that create-react-app automatically adds
  • The component file I was converting to TypeScript
  • One component spec that had a linting error

Alright, so let's walk through these changes.

Eslintrc
This file was pretty straightforward. I used most of the recommended settings and merged in the existing upper-level rules we had already in the codebase.

The main thing I wanted to point out was the fix that allowed me to import a single .tsx file into a .js file without getting a compilation or linting warning?

Two things made this work:

module.exports = {
  parser: '@typescript-eslint/parser',
  rules: {
    'import/extensions': ['.js', '.jsx', '.json', '.ts', '.tsx']
    ...
  },
  settings: {
    'import/resolver': {
      node: {
        paths: ['src'],
        extensions: ['.js', '.jsx', '.json', '.ts', '.tsx'],
      },
    },
  },
  ...
}

tsconfig
Since create-react-app generates this, it is challenging to alter it. I did add a few extra compilerOptions to fit the needs of our app, but did not change it in any way worth pointing out.

Package Changes
Most hanges in package.lock were to add new type definitions or new dependencies.

I also updated our linting script to include new .tsx files:

"lint": "eslint './src/**/*.js' './src/**/*.tsx'",

I did run into an issue where our eslint-plugin-jsx-a11y version was throwing a false-positive linting error. That was resolved by upgrading to: "eslint-plugin-jsx-a11y": "6.1.2",

The New Component

So what does a newly-converted component look like? I strategically picked the furthest leaf of a component node I could find - that is to say this component is only used in one place by one other component and renders one input. So it was simple to alter and had minimal impact on the app.

Here's a very generalized version of the component before TS:

import * as React from 'react'
import { Field } from 'formik'

export default function ComponentField({ prop1, prop2 }) {
  return (
    <div className={s.className}>
      <Field
        type="number"
        name={name}
        render={({ field }) => <input {...field} />}
      />
    </div>
  )
}

And here's what it looked like after TypeScript:


import * as React from 'react'
import { Field, FieldProps } from 'formik'

interface ComponentProps {
  prop1: boolean
  prop2: string
}

export default function ComponentField({
  prop1,
  prop2,
}: ComponentProps): JSX.Element {
  return (
    <div className={s.className}>
      <Field
        type="number"
        name={name}
        render={({ field }: FieldProps) => <input {...field} />}
      />
    </div>
  )
}

Resources I Found Helpful

  • This cheatsheet is extremely popular and even has a section on migrating!
  • Microsoft has a migration guide that might be helpful for you and has a dummy app you can follow along with
  • This Twitter thread about what challenges people faced while using React + TypeScript. And read the comments, too!

Conclusion

See?! Not so bad! This approach works well for our small team with devs who are unfamiliar with TypeScript. We'll be able to add and convert files as we go without the pressure of changing everything at once.

This also made a low-risk implementation for us - it was one file that we could test in isolation as opposed to renaming every file to .tsx, adding any all over the place and worrying about compilation errors or build issues.

I'm no expert, and implementing TypeScript into a legacy codebase totally depends on the setup of your app - but stick with it! You can figure it out.

Discussion

pic
Editor guide