Incremental migration of a “create-react-app” JavaScript project to TypeScript (without eject 🎆😉)

https://www.typescriptlang.org/Samples/

We are developing an Excel Addin using ReactJs+UI Fabric React. This application is working already in production but there are new features in our product roadmap. We started using Office UI Fabric React 6 which didn’t support TypeScript but this year Fabric React 7 was released and TypeScript is now supported! 🚀😂🤩

Now for us, these are good and bad news. We wanted to integrate the new version because of improvements and stability, but our code base is a little bit large. We evaluate to migrate everything to TypeScript but we do not have the time to do it. I did a little bit of research and think what can we do so we can at least prepare for a future migration but at the same time still working on new (pending) features 😵🥶. We have another applications in JavaScript that we want to migrate, so this could be a great moment to do something experimental.

The Excel Addin application we used as a start point was create with the classic create-react-app, which we migrated react-scripts to version 3.2.0 following the official docs.

Migrating to latest version of create-react-app it’s not painful, if you haven’t made major changes. You almost need to upgrade package.json, re-run yarn start or react-scripts start and follow the suggestions.

Now let’s start migrating our code base to TypeScript (incrementally of course 😉), but before we can start save your changes first please😇.

Project structure and packages

Our current project structure is similar to this (it is the default for create-react-app).

src/
//ALL JS+JSX FILES
.gitignore
package.json

The first step is to create a folder with name assets inside src folder, here we are going to move any css or png or any static file that is in src root. If you do not have files, just create this folder for future changes.

src/
assets
//ALL JS+JSX FILES
.gitignore
package.json

Fix your references in code if you move any file to assets folder. After this changes, copy the src folder to a folder called ts in the same path. Inside ts create a file with name tsconfig.json. Also create two more files at the same level that ts folder, this files should be named tsconfig.build.json and tsconfig.lint.json.

src/
assets
//ALL JS+JSX FILES
ts/
assets
//ALL JS+JSX FILES
tsconfig.json
tsconfig.build.json
tsconfig.lint.json
.gitignore
package.json

Now we need to install the following packages in order to integrate TypeScript:

yarn add typescript tslib 
yarn add --dev @types/node @types/react @types/jest @types/react-dom @types/react-redux

TypeScript configuration

Now let’s edit tsconfig.lint.json by adding the following content to this file. Please check that in outDir we are writting src because there we are going to ouput our transpiled files, so we will not eject our project. Check also baseUrl , this configuration will allow us to use non relative imports in TypeScript. Check include configuration, we are specifying our sources path. Finally check exclude configuration in which we are adding src in order to centralize our code in ts folder. This tsconfig.lint.json file is used for linting and checking .js and .ts files.

This file will not emit any code, it is used to analyze code only.

tsconfig.lint.json

{
"compilerOptions": {
"outDir": "src",
"module": "esnext",
"target": "ES6",
"lib": ["es6", "dom"],
"sourceMap": false,
"inlineSourceMap": true,
"allowJs": true,
"jsx": "preserve",
"moduleResolution": "node",
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"baseUrl": "ts",
"checkJs": false,
"resolveJsonModule": true,
"esModuleInterop": true,
"skipLibCheck": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"importHelpers": true
},
"include": ["ts"],
"exclude": ["node_modules", "build", "scripts", "acceptance-tests", "webpack", "jest", "src"],
"types": []
}

Depending of your configuration, you may want to specify the jsx value to preserve, react or react-native . According to docs:

TypeScript ships with three JSX modes: preserve, react, and react-native. These modes only affect the emit stage - type checking is unaffected. The preserve mode will keep the JSX as part of the output to be further consumed by another transform step (e.g. Babel). Additionally the output will have a .jsx file extension. The react mode will emit React.createElement, does not need to go through a JSX transformation before use, and the output will have a .js file extension. The react-native mode is the equivalent of preserve in that it keeps all JSX, but the output will instead have a .js file extension.

Output according to jsx flag.

Let’s edit tsconfig.build.json by adding the following content to this file. Please check that we are extending from tsconfig.lint.json in order to reuse the configuration that we already write. This file is used to generate code, therefore here we specify include again, but only for .ts files, the other files will be copied without almost any change, except sourcemaps.

tsconfig.build.json

{
"extends": "./tsconfig.lint.json",
"include": ["ts/**/*.ts", "ts/**/*.tsx"]
}

Finally let’s edit ts/tsconfig.json . This file, as we said, is inside ts folder.

This file is also used to trick VSCode, so when we open our project it will be fully analyzed. If you use another IDE, please verify your requirements.

ts/tsconfig.json

{
"extends": "../tsconfig.lint.json"
}

Cleaning and running

Delete all files and folders in srcexcept the assets folder. Then modify your .gitignore file (if necessary) to ignore the transpiled files.

src/**/*.js
src/**/*.json

Now to transpile our code, run in console the following command:

yarn tsc --project tsconfig.build.json --watch

And then start your project as you normally do, for example (using yarn or react-scripts depending of your configuration):

yarn start

Open your site and verify that everything is working fine and that’s all.

Our app running successfully

Testing

You can add a component just to verify that tsx files works, for example: let’s create for example a file in ts/components called tsx-test.tsx .

src/
assets
//ALL JS+JSX FILES
ts/
components/
tsx-test.tsx //just for testing purposes 😅

assets
//ALL JS+JSX FILES
tsconfig.json
tsconfig.build.json
tsconfig.lint.json
.gitignore
package.json

Write this content:

ts/components/tsx-test.tsx

import React from "react";class TsxProps {}function TsxTest(props: TsxProps) {
return <div>Hello from TS and JS project</div>;
}
export default TsxTest;

And add to any page or view that you have. It will be transpiled and accepted correctly by react-scripts:

A new view in Typescript

VSCode

You can build your project in VSCode as a standard task. You can add or modify your build task in VSCode (tasks.json) to include our new profile:

.vscode/tasks.json

{
"version": "2.0.0",
"command": "gulp",
"isBackground": true,
"echoCommand": false,
"type": "shell",
"windows": {},
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.build.json",
"option": "watch",
"problemMatcher": [
"$tsc-watch"
],
"group": "build"
}
]
}

Also we need to exclude the old files from VSCode explorer in settings.json:

.vscode/settings.json

{    
// ... more stuff ...
"files.exclude": {
// ... more files ...
"src/{[^a],?[^s],??[^s],???[^e],????[^t],?????[^s]}*": true
},
// ... more stuff ...
}

Final notes

✋ Maybe you will have to make some minor changes depending of your imports (relative or non relative).

👍 Non relative paths still working! For example we can do: import TsxTest from “components/tsx-test”;

👌 It’s probable that react-scripts start throws some warnings because of code style, but remember that now it is generated so do not worry (Instead it is an error not a warning)

That’s all folks! Feedback is welcome. Let’s keep learning.

Passionated developer