Converting a Create-React-App using Craco to TypeScript
Lori Baumgartner
Posted on November 12, 2019
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
thatcreate-react-app
used - Add the
tsconfig.json
-
yarn.lock
changes -
package.json
changes with new dependencies - A new
/react-app-env.d.ts
file thatcreate-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.
Posted on November 12, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.