loading...
Cover image for Incrementally migrating a CRA application to TypeScript without ejecting

Incrementally migrating a CRA application to TypeScript without ejecting

n1ru4l profile image Laurin Quast Updated on ・5 min read

Cover Art by 周 康

Update (29.02.2020):

I was notified by whatwhatwhatwhatwhut on Reddit
that it is way easier to add TypeScript support! We will just have to follow these steps mentioned in the documentation.

TL;DR:

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

Rename any file from .js to .ts or .tsx (and adjust types).

Restart the development server.

This is definitely an improvement of the somehow cumbersome method below. So far I did not notice any drawbacks!

Original Content:

JavaScript Tooling is constantly improving. The power of CRA makes scaffolding a new React project easier than ever before. Just a few years ago the modern JavaScript developer had to fiddle around with Webpack configs and whatnot. Nowadays higher-level abstractions such as CRA made all the configuring obsolete.

Another tooling that has made a buss in the community is TypeScript. Inevitable, CRA added TypeScript support without having to eject (and therefore avoid dealing with Webpack configuration again).

There is nothing wrong with learning how to configure Webpack (it even got way easier by providing better defaults in more recent versions), but still, each second spent on worrying about Webpack is stealing our precious time that could rather be spent building the product. But let's get back to the topic 😅.

Unfortunately, it is still hard to incrementally adopt TypeScript in an existing "vanilla" CRA project without having to eject or renaming all the files which will result in a giant pull request and conflict with everything other people are working on right now.

For an existing project, I tried a new approach that I want to share with you!

Bootstrapping the TypeScript Config

The basic idea is to have a new folder ts_src that holds all our new TypeScript code. When we compile that code we want the output files built into our src directory so that our CRA development server will pick up the changes and reload our application running inside the browser.

mkdir -p ts_src
touch ts_src/tsconfig.json
echo '
{
  "compilerOptions": {
    "module": "ESNext",
    "target": "esnext",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "noImplicitAny": false,
    "outDir": "./../src",
    "rootDir": "./",
    "sourceMap": true,
    "declaration": true,
    "strict": true,
    "allowSyntheticDefaultImports": true,
    "esModuleInterop": true,
    "strictNullChecks": true,
    "jsx": "react",
    "skipLibCheck": true,
    "moduleResolution": "node",
    "noEmit": true,
  }
}' > ts_src/tsconfig.json

Add TypeScript Build Script to package.json

We add one for building the whole application as well as one for development.

-  "build": "react-scripts build",
+  "build": "yarn build:ts && react-scripts build",
+  "build:ts": "yarn tsc --build src_ts/tsconfig.json",
+  "build:ts:watch": "yarn build:ts --watch",

Moving our file to ts_src

Then we can start moving existing files over to our new ts_src folder. We will keep the same folder structure as inside the src directory.

# ensure directory exists
mkdir -p $(dirname src_ts/path/to/file.js)
# move file to new path
mv src/path/to/file.js src_ts/path/to/file.ts

Add path src/path/to/file.js to .gitignore and .eslintignore

The file is now being built to src/path/to/file.js and therefore a build artifact, so there is no reason left for tracking the build artifact inside git.

We also must add the file to the .eslintignore because the react-scripts does also apply eslint on files listed in the .gitignore and the build artifacts could trigger eslint warnings/errors that will let our CI build fail.

echo 'src/path/to/file.js' >> .gitignore
echo 'src/path/to/file.js' >> .eslintignore
git rm --cached src/path/to/file.js

Repeat step for every file that is imported by src_ts/path/to/file.ts

Now we need to repeat the steps above for every file that is imported by the file we just moved over, otherwise, the TypeScript compiler will complain 😅. Ideally, we would start with migrating smaller files, that do not have many imports.

Add typings

Then we can finally add our types! I already caught some nasty bugs in some of the code I migrated by just adding types! Let me know what you encountered along the way in the comments 😉

Committing the changes changes

Nice, we just migrated a part of our application over to TypeScript.

For faster development iteration we can use the TypeScript compiler in watch mode (besides the yarn start command that spins up the CRA development server):

yarn build:ts:watch

Adding new functionality

We can now add new features in TypeScript inside our src_ts folder.

Before we create a new file we should also make sure the corresponding path inside src is not already taken by an existing file!

Someday in the future: the src folder is empty 🎉

Depending on the project size this could take a while, but hopefully, that day will finally come!

The migration is now almost over, there are no more JavaScript source files available and everything is now typed!

We can now delete the empty src folder and rename the ts_src to src.

rmdir src # ensure it is empty ;)
mv ts_src src

Next, we can move the TypeScript config to the root of the repository.

mv src/tsconfig.json tsconfig.json

And also apply some adjustments:

  {
    "compilerOptions": {
      "module": "ESNext",
      "target": "esnext",
      "lib": [
        "dom",
        "dom.iterable",
        "esnext"
      ],
       "noImplicitAny": false,
-      "outDir": "./../src",
-      "rootDir": "./",
       "declaration": true,
       "strict": true,
       "allowSyntheticDefaultImports": true,
       "esModuleInterop": true,
       "strictNullChecks": true,
       "jsx": "react",
       "skipLibCheck": true,
       "moduleResolution": "node",
       "noEmit": true,
-    }
+    },
+    "include": [
+     "src"
+    ]
  }

Then we can also finally remove all this .eslintignore and .gitignore entries we collected while migrating existing .js files (or adding new .ts files).

We are done 👌

This alternative approach might have some drawbacks such as adding entries to .eslintignore and .gitignore or having to migrate all files that depend on each other at once, but I think this is a great alternative towards traditional ejecting.

Many of the tasks could be enhanced further e.g. by creating a handy CLI that will automatically run the commands required for moving a file from src to src_ts (along with its imported files) while also adding the corresponding entries to the .eslintignore and .gitignore files.

In case I further explore such ideas I will definitely write about it or even publish it as an npm package.

Do you know a better method for migrating the project? Any other ideas on how the migration path can be improved further? Are you currently planning to migrate an application to TypeScript?

Let me know, down in the comments ⬇ and thank you so much for reading!

Discussion

markdown guide
 

Nice post. I'm actually looking at migrating a project to TypeScript myself, and this would indeed enable a gradual move over.

Do you see any differences if we would be using .tsx files instead of .ts files?

Thanks!

 

It totally works! Typescript will compile .tsx files to .js.

I am applying this practice to an open source project, so if you are interested you can find the initial changes in this PR: github.com/dungeon-revealer/dungeo...

Happy migrating! Oh, and it would be awesome if you could let me know whether this workflow works with you 🙂.

 

You should definitely make the CLI, I'd use it for sure.