This tutorial guides you through converting a React Native project to use TypeScript. It includes details on how to configure linting, troubleshoot common errors and refactor an existing project.
In this tutorial, we’re going to take a look at how you can use TypeScript for building your React Native projects. Specifically, we’re going to take a look at the following:
This tutorial assumes that you’ve already weighed the pros and cons of using TypeScript over Flow. So I’m not going to sell you in the idea that TypeScript is better than Flow. The aim of this tutorial is just to get you up and running with TypeScript in React Native quickly.
You can view the source code used in this tutorial on this GitHub repo. The starter project which contains a standard React Native project is on the standard-rn
branch. While the branch containing the final output of this tutorial is the typescript
branch. The starter
branch simply contains a sample of how your project will look like when you use the TypeScript template to initialize a new React Native project. The only change I made to it is adding the Gradle 3 configuration.
To follow this tutorial, you need to have basic knowledge of developing React Native apps.
I assume you’ve set up the React Native development environment on your machine. I’ve used Node version 8.3.0 for testing, so anything higher than that should also be good. You’ll also need Yarn, mine is at 1.7.0.
You also need to know your way around the text-editor you are using. We’re specifically going to use Sublime Text in this tutorial, but there should be an equivalent for the text editor you’re using as well. That’s why at the end of the section for setting up Sublime Text for TypeScript development, I’m going to point out some links that will help you do a similar setup to what we’re going to do here.
The project that we will be working with is a simple camera app. The only thing it does is allow the user to take a picture and then preview it. Here’s how it looks like:
The easiest way to get the project is by cloning the repo and switching to the starter
branch:
1git clone https://github.com/anchetaWern/RNTypeScript.git 2 cd RNTypeScript 3 git checkout standard-rn
The standard-rn
branch contains the plain React Native project which we’ll convert to use TypeScript. This means we’re not actually going to use it as a base of the project. We’re simply going to copy the existing code over to the project that we’re going to initialize.
Inside another directory, create a new React Native project which uses TypeScript:
1react-native init RNTypeScript --template typescript && node RNTypeScript/setup.js
As you can see from the command above, it’s like setting up a plain React Native project. The only difference is you’re specifying a --template
option. This allows you to customize which template you want your project to use. This option has been around since the 0.42 release. The typescript
template that we’re using is created by Emin Khateeb. This method is by far the simplest and easiest way to get started with TypeScript in React Native.
We’ll go through this template later, so you know what it’s actually doing, and how is it set up.
Once the project is created, copy the following files (and folders) from the repo you cloned earlier:
src
App.js
After copying them over, delete the existing App.tsx
file then rename the App.js
file to App.tsx
. Use the same .tsx
file extension for the .js
files inside the src
directory as well. Note that the existing JavaScript code are valid TypeScript code as well. This means you don’t have to update the code just to get it to work. Though we’ll refactor the code in a later section so we can benefit from TypeScript’s type-checking capabilities. And also to fix some of the linting errors that will surely show once we’ve setup linting on Sublime Text.
Next, in the root directory of the project you created, install react-native-camera:
1yarn add react-native-camera@1.1.4
Once that’s done, follow the installation instructions on the GitHub repo of the module. For both platforms, follow the requirements section first. After that, read through the following links based on the platform you will be running the app on:
If you’re only testing on Android, you can safely follow the automatic install for Android. We’re only doing the manual install for Android because we don’t want the automatic install to affect iOS. This is because react-native link
affects both platforms.
Note: don’t follow the very first step, the one for installing react-native-camera with npm since we already installed it with Yarn. Also, if you’re following the manual install for Android, only follow until the 6th step. Don’t follow anything after that.
Next, add prop-types. This is used for implementing type-checking for props. This will no longer be used once we refactor the project to use TypeScript. But we still have to install it because the project we’re starting with still uses it:
1yarn add prop-types
Lastly, install tslint-react as a dev dependency. This allows us to lint React code:
1yarn add --dev tslint-react
The React Native Camera module uses Gradle 3 for building its source. So for the project to work, we need to do the same as well. To do this, you can copy the following files from the project you cloned earlier over to the React Native project you initialized:
android/build.gradle
android/gradle/wrapper/gradle-wrapper.properties
Once that’s done, you should be able to run the project:
1react-native run-android 2 react-native run-ios
If you get an error similar to the following when you run the app on Android:
The solution is to set the exifinterface
to be the same version as the runtime version indicated in the error:
1// file: android/app/build.gradle 2 3 dependencies { 4 // previously added dependencies here... 5 6 compile (project(':react-native-camera')) { 7 exclude group: "com.google.android.gms" 8 compile 'com.android.support:exifinterface:25.+' // update 25.+ to 26.+ 9 } 10 11 // other dependencies here... 12 }
Now we’re ready to figure out what makes the TypeScript template work.
If you compare the package.json
file of the standard React Native project from the one that uses the TypeScript template, you’ll notice a few differences.
First, under the devDependencies
we have a few of these @types
modules:
1"devDependencies": { 2 "@types/jest": "^23.3.0", 3 "@types/react": "^16.4.6", 4 "@types/react-native": "^0.56.2", 5 "@types/react-test-renderer": "^16.0.1" 6 }
These are the type declaration files for the libraries that we’re depending on. This allows TypeScript to understand the types used in those libraries. So that when you use them, it knows whether you’ve used them correctly or not. For example, when you call a function from a specific library, the TypeScript linter will know whether you supplied the correct types for the arguments. If you’re still confused, here’s a good StackOverflow answer on what typings are.
Next, we have the react-native-typescript-transformer
. This is the Babel transformer for transforming your .tsx
code so that it can be used within the React Native environment:
1"react-native-typescript-transformer": "^1.2.10",
Next is ts-jest
. It’s a preprocessor for testing code (that uses Jest) written in TypeScript. In simple terms, it’s what converts your TypeScript testing files to something that Jest understands. We won’t really cover testing in this tutorial, but this is good to know because you’ll want to write your tests in TypeScript as well:
1"ts-jest": "^23.0.0",
The last one is the actual typescript
library. This is what allows us to use TypeScript’s features (for example: type-checking):
1"typescript": "^2.9.2"
In case, you’re wondering, here’s the source of the devDependencies
that was added in the TypeScript template.
Next, open the rn-cli.config.js
file. This file is checked by the React Native CLI whenever the metro bundler runs. Here we’re telling the CLI to use the react-native-typescript-transformer whenever the bundler encounters .ts
or .tsx
files:
1module.exports = { 2 getTransformModulePath() { 3 return require.resolve('react-native-typescript-transformer'); 4 }, 5 getSourceExts() { 6 return ['ts', 'tsx']; 7 }, 8 };
Next is the tsconfig.json
file. This allows you to specify the options to be used for compiling the project. You can find more information about it here:
1{ 2 "compilerOptions": { 3 "allowSyntheticDefaultImports": true, 4 "esModuleInterop": true, 5 "jsx": "react-native", 6 "lib": ["es6"], 7 "module": "es6", 8 "moduleResolution": "node", 9 "noEmit": true, 10 "noImplicitAny": true, 11 "target": "es6" 12 }, 13 "exclude": ["node_modules"] 14 }
Lastly, create the tslint.json
file. This file contains the TSLint rules. If you’re using ESLint for linting your code, this is the equivalent config file of the .eshintrc
file:
1{ 2 "extends": ["tslint:latest", "tslint-react"], // the default rules we want to extend 3 "rules": { 4 "ordered-imports": [true], // import statements should be alphabetically arranged 5 "member-access": [false], // don't require explicit visibility declarations for class members 6 "member-ordering": [false], // don't check for keeping related groups of classes together 7 "trailing-comma": false, 8 "no-empty": false, // allow empty blocks 9 "no-submodule-imports": false, // allow importing of submodules 10 "no-implicit-dependencies": false, // allow importing of modules that aren't listed under the package.jsons dependency list 11 "no-constant-condition": false, 12 "triple-equals": [true, "allow-undefined-check"], // always use triple equals when comparing values, except for checking undefined values 13 "arrow-parens": [false], // disable checking of parenthesis for single argument arrow functions 14 "semicolon": [true, "always", "ignore-bound-class-methods"], // disable checking of semi-colon for class bound methods 15 "object-literal-sort-keys": false, 16 "no-duplicate-imports": true, // disallow repeating of imports from the same module 17 // react specific rules (more info here: https://github.com/palantir/tslint-react#rules) 18 "jsx-alignment": true, // enforces a consistent style for multiline JSX elements 19 "jsx-no-bind": true, // disallow function binding in JSX attributes 20 "jsx-no-lambda": true, // disallow creation of anonymous functions inside the render method 21 "jsx-no-multiline-js": false, // disable checking of multi-line JavaScript inside JSX 22 23 "max-classes-per-file": [false] // disable rule for limiting the number of classes per file 24 } 25 }
The above rules are a bit relaxed so you should configure it according to your project’s style guide. I’ve set false
for some of these rules. This is because tslint:latest
and tslint-react
are a little too rigid. Some of their rules are overkill for this project, that’s why we’re disabling them.
You can find the default rules set by TSLint here. If you want to know more about a specific rule that I’ve used above, you can use the following URL to read all about it:
1https://palantir.github.io/tslint/rules/{rule-name}
In this section, we’re going to set up Sublime Text so that we can see the linter errors right in the gutter. If you’re not using Sublime Text, fret not because I’ll be linking to some tutorials which shows you how to setup TypeScript for supported text editors.
The first thing that you need to do is to make sure you have a supported version of Node running on your machine. I personally use nvm to easily manage Node versions. Here’s how I would set my machine to use version 8.3.0 of Node:
1nvm install 8.3.0 2 nvm alias default 8.3.0 3 nvm use default
Note that TSLint requires Node version 8 and above. So you should be okay with any Node version which falls within that requirement.
On Sublime Text, install the following packages using package control:
Next, go to Preferences → Package Settings → SublimeLinter → Settings and set the lint_mode
. This will trigger the linter to check your file whenever you open a file or when you save it:
1{ 2 // previously added settings here... 3 4 // add this: 5 "lint_mode": "load_save" 6 }
Once that’s done, you should start seeing errors on Sublime Text’s gutter when you save a file:
If the indicators aren’t showing up in the gutter, the problem is most commonly that SublimeLinter isn’t able to read the location of Node.js. To solve that, go to Preferences → Package Settings → SublimeLinter → Settings and add the following. Replace the PATH
with the path to the Node.js executable file:
1{ 2 // previously added settings here... 3 4 // add these: 5 "debug": true, // enable debugging 6 "linters": { 7 "tslint": { 8 "env": {"PATH": "/path/to/nodejs/executable_file"} 9 } 10 } 11 }
Once that’s done, restart the text editor and the issue should be fixed. If it still doesn’t work, check the console by clicking on View → Console. From there, you can check the errors by making a change to the App.tsx
file and save it. That should trigger SublimeLinter to execute.
Another common issue is that the issues are not indicated in the gutter. That can be solved by installing ImageMagick. Here’s the command for installing it on Mac:
1brew install imagemagick
If you’re on Ubuntu, you can install ImageMagick with the following:
1sudo apt-get install imagemagick
If it still doesn’t work, try searching for the issue on Google or check the SublimeLinter troubleshooting page.
If you’re using another text-editor, here are some links which might help:
Now it’s time for us to refactor the code for the project. The first thing that we’re going to do is to fix the linter errors. And if there are still improvements we can do after that, then we’ll also update the code.
Before proceeding, if you’re using any other linters or plugins (for example: ESLint) that might interfere with TSLint, you should disable them in your project settings file first:
1// file: .sublime-project 2 { 3 "folders": 4 [ 5 { 6 "path": "." 7 } 8 ], 9 "settings": { 10 "SublimeLinter.linters.eslint.disable": true // disable eslint 11 } 12 }
Now we’re ready to start refactoring. Start by opening the App.tsx
file. The first error that you’ll encounter is “use an interface instead of a type literal”.
This can be solved by using interface
instead of type
:
1// type Props = {}; 2 interface Props {} // use this in place of the one on the top
This is the way you specify prop types in TypeScript. But once you update it, there will be two new issues that will show up:
The first one is the default style implemented by TypeScript. It means that interface names should be prefixed with a capital “I”. If you ask me, I’d rather stick with any name that I want. So to disable that, add the following rule on the tslint.json
file:
1"interface-name": false,
As for the second issue, it means that we can’t have an empty interface. The App.tsx
file doesn’t really accept any props so the best way to solve it is to remove the prop type declaration altogether:
1// interface Props {} // remove this 2 export default class App extends Component { 3 // rest of the code here... 4 }
Once that’s done, App.tsx
should be free of issues.
Next, let’s take a look at src/components/ActionButton.tsx
. If you’re like me, you will most likely see the following issue: “space indentation expected”.
This can be solved by installing the EditorConfig package on Sublime Text. This package allows you to automatically format your code based on a specific configuration. This package came out way before Prettier became a thing, but still useful to this day for simple things like this.
Once installed, create an .editorconfig
file at the root of your project and add the following:
1root = true 2 3 [*] 4 end_of_line = lf 5 insert_final_newline = false 6 charset = utf-8 7 trim_trailing_whitespace = true 8 quote_type = double 9 10 [*.tsx] 11 indent_style = space 12 indent_size = 2
This will make sure all your .tsx
files are indented using two spaces.
Once that’s done, close the src/components/ActionButton.tsx
file, re-open it, then hit save. That should make all the issues go away. At this point, you can also open the src/components/Card.tsx
file and re-save it. It has basically the same issues as the ActionButton so that should solve all the issues as well.
If that doesn’t solve your issue, it might be that you have Prettier configured on your text-editor, and it’s probably fighting with your TSLint config. So the solution is to either update your .prettierrc
file to use the same style that your tslint.json
file enforces, or update tslint.json
to ignore the style used by Prettier.
The last thing we’re going to do is refactor the code of the two components: Card
and ActionButton
. Prop types are great for validating the props on run-time, but since we’re already using TypeScript, we can actually refactor the code so that the validation happens while you’re still writing the code.
First, open the src/components/Card.tsx
and src/components/ActionButton.tsx
then remove all the code related to setting the prop types:
1import PropTypes from "prop-types"; // remove from both files 2 3 // remove from: src/components/Card.tsx 4 Card.propTypes = { 5 image: PropTypes.string.isRequired, 6 label: PropTypes.string.isRequired 7 }; 8 9 // remove from: src/components/ActionButton.tsx 10 ActionButton.propTypes = { 11 label: PropTypes.string.isRequired, 12 action: PropTypes.func.isRequired 13 };
Next, you can specify an interface in place of the prop types that you just removed. This requires both the image
and label
prop to be added whenever you use the Card
component:
1// src/components/Card.tsx 2 interface CardProps { 3 image: string; 4 label: string; 5 } 6 7 const Card: React.SFC<CardProps> = ({ image, label }) => { 8 return ( 9 // previous return code here... 10 ); 11 }
Do the same for the other component:
1// src/components/ActionButton.tsx 2 interface ActionButtonProps { 3 label: string; 4 action: () => void; // can be any function which doesn't change anything 5 } 6 7 const ActionButton: React.SFC<ActionButtonProps> = ({ label, action }) => { 8 return ( 9 // previous return code here... 10 ); 11 }
Note that if you try to use the Card
component without specifying the required props, it doesn’t actually trigger the linter to issue a warning. This is one of the limitations I found with TSLinter for Sublime Text.
If you do the same thing on Visual Studio Code though, we see the expected behavior:
Here are a few resources to learn more about using TypeScript in React Native:
That’s it! In this tutorial, you’ve learned how to setup your React Native app to use TypeScript. As you have seen, using TypeScript comes with the benefit of linting your code, static-typing, and overall, better code quality.
You’ve also seen that there are limitations when it comes to linting your code in Sublime Text. So if you’re open to using another text-editor, I recommend you to use Visual Studio Code for any type of TypeScript development. Because it has built-in support for working with TypeScript, and more features will be available for working with TypeScript when you install plugins such as TSLint.
You can view the source code used in this tutorial on this GitHub repo.