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.
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.
++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:
Create TypeScript config file in your root directory.
++pre>++code class="language-bash">touch tsconfig.json ++/pre>++/code>
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:
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.
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.
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.
ts-migrate is a cli developed by airbnb that will help you migrate your codebase from JavaScript to TypeScript. It will:
++pre>++code class="language-javascript">yarn add -D ts-migrate ++/pre>++/code>
++pre>++code class="language-javascript">npx -p ts-migrate -c "ts-migrate-full ."++/pre>++/code>
Use the default answers for each question asked.
++pre>++code class="language-javascript">yarn remove ts-migrate ++/pre>++/code>
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.
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!
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 ☠️
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:
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!