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:

  • Setting up a new React Native project that uses TypeScript.
  • Setting up linting and auto-completion of TypeScript code for Sublime Text.
  • Refactoring the code of an existing project to use TypeScript.

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.

Prerequisites

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.

Setting up the project

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:

react-native-typescript-demo

The easiest way to get the project is by cloning the repo and switching to the starter branch:

    git clone https://github.com/anchetaWern/RNTypeScript.git
    cd RNTypeScript
    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:

    react-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.

Installing the dependencies

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:

    yarn 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:

    yarn add prop-types

Lastly, install tslint-react as a dev dependency. This allows us to lint React code:

    yarn add --dev tslint-react

Upgrading to Gradle 3

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:

    react-native run-android
    react-native run-ios

Troubleshooting

If you get an error similar to the following when you run the app on Android:

exifinterface-conflict

The solution is to set the exifinterface to be the same version as the runtime version indicated in the error:

    // file: android/app/build.gradle

    dependencies {
      // previously added dependencies here...

      compile (project(':react-native-camera')) {
        exclude group: "com.google.android.gms"
        compile 'com.android.support:exifinterface:25.+' // update 25.+ to 26.+
      }

      // other dependencies here...
    }

Breaking down the TypeScript template

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:

    "devDependencies": {
      "@types/jest": "^23.3.0",
      "@types/react": "^16.4.6",
      "@types/react-native": "^0.56.2",
      "@types/react-test-renderer": "^16.0.1"
    }

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:

    "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:

    "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):

    "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:

    module.exports = {
      getTransformModulePath() {
        return require.resolve('react-native-typescript-transformer');
      },
      getSourceExts() {
        return ['ts', 'tsx'];
      },
    };

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:

    {
      "compilerOptions": {
        "allowSyntheticDefaultImports": true,
        "esModuleInterop": true,
        "jsx": "react-native",
        "lib": ["es6"],
        "module": "es6",
        "moduleResolution": "node",
        "noEmit": true,
        "noImplicitAny": true,
        "target": "es6"
      },
      "exclude": ["node_modules"]
    }

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:

    {
      "extends": ["tslint:latest", "tslint-react"], // the default rules we want to extend
      "rules": {
        "ordered-imports": [true], // import statements should be alphabetically arranged
        "member-access": [false], // don't require explicit visibility declarations for class members
        "member-ordering": [false], // don't check for keeping related groups of classes together
        "trailing-comma": false,
        "no-empty": false, // allow empty blocks
        "no-submodule-imports": false, // allow importing of submodules
        "no-implicit-dependencies": false, // allow importing of modules that aren't listed under the package.jsons dependency list
        "no-constant-condition": false,
        "triple-equals": [true, "allow-undefined-check"], // always use triple equals when comparing values, except for checking undefined values
        "arrow-parens": [false], // disable checking of parenthesis for single argument arrow functions
        "semicolon": [true, "always", "ignore-bound-class-methods"], // disable checking of semi-colon for class bound methods
        "object-literal-sort-keys": false,
        "no-duplicate-imports": true, // disallow repeating of imports from the same module 
        // react specific rules (more info here: https://github.com/palantir/tslint-react#rules)
        "jsx-alignment": true, // enforces a consistent style for multiline JSX elements
        "jsx-no-bind": true, // disallow function binding in JSX attributes
        "jsx-no-lambda": true, // disallow creation of anonymous functions inside the render method
        "jsx-no-multiline-js": false, // disable checking of multi-line JavaScript inside JSX

        "max-classes-per-file": [false] // disable rule for limiting the number of classes per file
      }
    }

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:

    https://palantir.github.io/tslint/rules/{rule-name}

Setting up Sublime Text for TypeScript

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:

    nvm install 8.3.0
    nvm alias default 8.3.0
    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:

  • TypeScript Syntax – for adding syntax highlighting for TypeScript code.
  • SublimeLinter – main framework for working with linters.
  • SublimeLinter-tslint – for linting TypeScript code.

Next, go to PreferencesPackage SettingsSublimeLinterSettings and set the lint_mode. This will trigger the linter to check your file whenever you open a file or when you save it:

    {
      // previously added settings here...

      // add this:
      "lint_mode": "load_save"
    }

Once that’s done, you should start seeing errors on Sublime Text’s gutter when you save a file:

tslint-errors

Troubleshooting SublimeLinter

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 PreferencesPackage SettingsSublimeLinterSettings and add the following. Replace the PATH with the path to the Node.js executable file:

    {
      // previously added settings here...

      // add these:
      "debug": true, // enable debugging
      "linters": {
        "tslint": {
          "env": {"PATH": "/path/to/nodejs/executable_file"}
        }
      }
    }

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 ViewConsole. 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:

    brew install imagemagick

If you’re on Ubuntu, you can install ImageMagick with the following:

    sudo apt-get install imagemagick

If it still doesn’t work, try searching for the issue on Google or check the SublimeLinter troubleshooting page.

Setting up TypeScript for other text-editors

If you’re using another text-editor, here are some links which might help:

Refactoring the project

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:

    // file: .sublime-project
    {
      "folders":
      [
        {
          "path": "."
        }
      ],
      "settings": {
        "SublimeLinter.linters.eslint.disable": true // disable eslint
      }
    }

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:

    // type Props = {};
    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:

  • Interface name must start with a capitalized ‘”I”
  • An empty interface is equivalent to “{}”

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:

    "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:

    // interface Props {} // remove this
    export default class App extends Component {
      // rest of the code here...
    }

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:

    root = true

    [*]
    end_of_line = lf
    insert_final_newline = false
    charset = utf-8
    trim_trailing_whitespace = true
    quote_type = double

    [*.tsx]
    indent_style = space
    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.

Refactoring PropTypes

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:

    import PropTypes from "prop-types"; // remove from both files

    // remove from: src/components/Card.tsx
    Card.propTypes = {
      image: PropTypes.string.isRequired,
      label: PropTypes.string.isRequired
    };

    // remove from: src/components/ActionButton.tsx
    ActionButton.propTypes = {
      label: PropTypes.string.isRequired,
      action: PropTypes.func.isRequired
    };

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:

    // src/components/Card.tsx
    interface CardProps {
      image: string;
      label: string;
    }

    const Card: React.SFC<CardProps> = ({ image, label }) => {
      return (
        // previous return code here...
      );
    }

Do the same for the other component:

    // src/components/ActionButton.tsx
    interface ActionButtonProps {
      label: string;
      action: () => void; // can be any function which doesn't change anything
    }

    const ActionButton: React.SFC<ActionButtonProps> = ({ label, action }) => {
      return (
        // previous return code here...
      );
    }

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:

required-prop-warning

Further reading

Here are a few resources to learn more about using TypeScript in React Native:

Conclusion

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.