React Native

How to migrate an existing React Native project from JavaScript to TypeScript

I have been using TypeScript for four years. It's my first protective barrier against bugs. When I start working on a legacy JavaScript project, my first move is to migrate it to Typescript! The setup is really fast and the benefits are high. I'll explain in this article my migration workflow.

Why you should use TypeScript

  • TypeScript is a superset of JavaScript that provides static type analysis. 'undefined' is not a function will not flood your console anymore. It's the fastest and cheapest test to detect specific errors.
  • TypeScript is integrated with many IDE. (see VSCode dedicated page). You have instant feedback on the code you write thanks to a TypeScript server running in the background and IntelliSense.
  • TypeScript is easy to adopt when you come from JS. You do not have to enable all rules at first, and many types can be inferred. You can deep dive into complex TypeScript patterns only when you are ready.
  • TypeScript typing system is extremely powerful. You can express many constraints with features like Template Literal Types and Mapped Types.

  • Powerful librairies like zod gives you the possibility to validate JS objects against schemas and infer the static TypeScript type from the schema. Your codebase will be protected from external dependencies and you'll have confidence in the object you manipulate.

Set-up guide

All the code snippets used in this paragraph were tested with typescript@4.8.4 and react-native@0.70.3. It may not work out of the box for later versions.

Step 1: Add missing TypeScript and types dependencies

++pre>++code class="language-javascript">yarn add -D typescript @types/jest @types/react @types/react-native @types/react-test-renderer ++/pre>++/code>
⚠️ Be careful, @types/* versions should match the libraries versions you are using in your project.

For example, at the time of writing, react-native@0.70.3 comes with jest@26.6.3. However @types/jest will resolve to version 29.2.0 instead of 26.0.14.

As a rule of thumb, for the selection of @types/* version number, you should:

  • Follow the same major version
  • Follow the same minor version when possible as APIs may change between minors
  • Use the latest patch version available

Step 2: create tsconfig.json file

Create TypeScript config file in your root directory.
++pre>++code class="language-bash">touch tsconfig.json ++/pre>++/code>

Step 3: Configure tsconfig.json

Copy/paste the following configuration in your tsconfig.json file.

++pre>++code class="language-javascript">{
 "compilerOptions": {
   "allowJs": true,
   "allowSyntheticDefaultImports": true,
   "esModuleInterop": true,
   "isolatedModules": true,
   "jsx": "react-native",
   "lib": ["esnext"],
   "types": ["react-native", "jest"],
   "moduleResolution": "node",
   "noEmit": true,
   "strict": true,
   "target": "esnext",
   "noImplicitReturns": true,
   "resolveJsonModule": true,
   "skipLibCheck": true,
   "noUncheckedIndexedAccess": true,
   "forceConsistentCasingInFileNames": true,
   "noFallthroughCasesInSwitch": true,
   "noImplicitOverride": true,
   "allowUnreachableCode": false
 },
 "exclude": [
   "node_modules",
   "babel.config.js",
   "metro.config.js",
   "jest.config.js"
 ]
} ++/pre>++/code>

This configuration is more restrictive than the one generated by react-native-cli with the TypeScript template. Its purpose is to provide better type safety. Here are some compiler options that are worth mentioning:

noUncheckedIndexedAccess

By default, when accessing an item in an array, TypeScript will type the result as defined:

++pre>++code class="language-javascript">const array = [0, 1, 2, 3];
const item = array[4]; // item type is "number" ++/pre>++/code>
However, depending on the length of the array, the result can be undefined. noUncheckedIndexedAccess will fix this behavior by setting the result as possibly undefined:
++pre>++code class="language-javascript">const array = [0, 1, 2, 3];
const item = array[4]; // item type is "number | undefined"++/pre>++/code>
On a legacy project, I found 10 bugs related to bad array accesses thanks to this rule. It's a must-have in your TypeScript config.

forceConsistentCasingInFileNames

With the advent of Expo, MacOS is not the only OS used by React Native developers anymore. Case sensitivity rules of the file system is dependent on the OS, and TypeScript relies on it. If I create a file myFile.ts , the import import { a } from "./myfile" may work in a case-insensitive OS but it will fail in a case-sensitive one. Activating forceConsistentCasingInFileNames compiler option improves your codebase consistency and protects your team against problems related to case-sensitive imports.

noFallthroughCasesInSwitch

switch statement comes with a behavior called fall-through. At first glance, fall-through seems handy but in the long term, It's often a source of bugs that are hard to catch.  noFallthroughCasesInSwitch set to true will force a non-empty case inside a switch statement to include either break or return.

Step 4: Migrate your JavaScript code to TypeScript

ts-migrate is a cli developed by airbnb that will help you migrate your codebase from JavaScript to TypeScript. It will:

  • rename your files from .js and .jsx extensions to .ts and .tsx extensions
  • infer types when possible or fallback to any
  • ignore all TypeScript errors with @ts-expect-error directive

Add ts-migrate dependency

++pre>++code class="language-javascript">yarn add -D ts-migrate ++/pre>++/code>


Run ts-migrate

++pre>++code class="language-javascript">npx -p ts-migrate -c "ts-migrate-full ."++/pre>++/code>

Use the default answers for each question asked.


Remove ts-migrate dependency

++pre>++code class="language-javascript">yarn remove ts-migrate ++/pre>++/code>

Step 5: Add a test script to your package.json

Inside your package.json file, add the following script:

++pre>++code class="language-javascript">{
scripts: {
"test:types": "tsc”
}
} ++/pre>++/code>

It will warn you if the compiler has found any TypeScript errors according to your configuration. This script must be run in your CI: you do not want to introduce TypeScript errors in your codebase ❌.

⚠️ Your basic project setup is now complete. However, you may still have to configure additional tools like Jest. Once It’s done, you should merge these changes as soon as possible. Keeping this branch up to date with the main branch will be hard. Do not wait for the migration to be fully completed. You can add all the types later.


TypeScript migration monitoring

ts-migrate is a powerful tool to accelerate the adoption of TypeScript in a codebase. However, depending on the number of @ts-expect-error and any added to your codebase, the way may be long until your code is fully typed. I advise you to keep track of two indicators, the number of @ts-expect-error in your codebase and your type coverage!

Monitor the number of @ts-expect-error in your codebase

Create a new file scripts/tsExpectErrorCommentsCount.sh with the following content:
++pre>++code class="language-javascript">#! bin/bash

grep -r '@ts-expect-error' -h ./src |   # Find all lines containing "@ts-expect-error"
   sed -E 's/^.*\/[*/] | \*\/|,//g' |  # Remove whitespace and brackets
   wc -l                               # Count number of lines ++/pre>++/code>

It will give you the number of @ts-expect-error found inside your codebase (here, inside src folder). You can then execute this script once a week to keep track of this number. Our target should be 0 ☠️

https://www.redacted.cyrilbo.com/content/images/2022/10/image-1.png

Monitor your type coverage

The type coverage of your codebase is the proportion of identifiers ( const myIdentifier = 1 ) for which the type is known by the compiler (either implicite or inferred). Our type coverage target should be 100%.

To compute this result, you can use a command line tool like typescript-coverage-report:

https://github.com/alexcanessa/typescript-coverage-report

It will generate a report that you'll be able to use to identify files that require your attention:

https://www.redacted.cyrilbo.com/content/images/2022/10/image-2.png

It may take a really long time before your codebase is fully migrated. However, the benefits of using TypeScript will show from the very beginning!

Développeur mobile ?

Rejoins nos équipes