Using TypeScript with React Native

I've recently been getting familiar with React Native. I have a background in using TypeScript with my Cordova projects, and I wanted to try it out with React Native.

I've put together a simple repository on GitHub that shows how this can be done. This blog post walks through the process step by step, and includes a link to the associated commit on GitHub. This should make it easy to see the changes via a diff for each step, even if you don't want to follow along.

This tutorial was written for Visual Studio Code on MacOS, but it is applicable to any IDE on Windows and Linux as well.

Set up your environment

First, set up React Native as outlined on the official React Native Getting Started page. This basically involves installing Node and the React Native CLI tools.

Once installed, you should verify that you can launch the sample project as described in Testing Your React Native Installation.

Note: Throughout the rest of this tutorial I use localy installed node modules rather than global. To make this easier you can add ./node_modules/.bin to your path. To do so you can type PATH=$PATH:./node_modules/.bin into your console.

This will allow you to invoke the TypeScript compiler locally via tsc instead of ./node_modules/.bin/tsc.

Create your project

First, we'll create a new empty project using the React Native CLI tools.

$ react-native init SampleApp1
$ cd SampleApp1

Again, you should verify your React Native installation is set up properly by running the sample project on a simulator or physical device (e.g., react-native run-ios).

Install TypeScript

First, we'll install the TypeScript and initialize a TypeScript project configuration file.

$ npm install typescript --save

$ tsc --version
Version 1.8.10

$ tsc init
message TS6071: Successfully created a tsconfig.json file.  

The tsc init creates a tsconfig.json file that is used to indicate a TypeScript project and specifies parameters for the TypeScript compiler. We'll leave this file with its defaults for now.

Next, we'll install Typings, a command line tool that will allow us to retrieve TypeScript definition files for React Native.

$ npm install typings --save

$ typings --version
1.3.2

$ typings init

The typings init file creates a typings.json file, which will indicate the type definitions we want to download.

See the diff for this step on GitHub

Set up the TypeScript compiler

Modify .gitignore

We're going to set up our TypeScript to output its compilation artifacts to an artifacts directory. In addition, TypeScript definition files will be downloaded to a typings directory by the Typings command line tool. Both directories contain files that we do not need to commit to source control, so we'll add these to the .gitignore file:

# TypeScript artifacts
artifacts  
typings  

Get type definitions

Since React and React Native are not written in TypeScript, we need to download type definitions for these libraries so the TypeScript compiler can know about them.

$ typings install dt~react --global --save
$ typings install dt~react-native --global --save

The dt~ prefix indicates the source of the type definitions, in this case Definitely Typed.

The --save flag writes these type definition references to the typings.json file, so we can restore them later using the typings install command.

Finally, the --global flag is used to indicate that these type definitions are global as opposed to proper external modules. For more information, check out the Typings FAQ.

Edit tsconfig.json

Now we'll make some changes to the TypeScript project file we created earlier.

First, we'll set the target to es6 instead of the default es5. The React Native CLI uses Babel to transform ES6 code into ES5 for execution on the device's native JavaScript runtime. Therefore, we want the output of our TypeScript code to be ES6 for use by Babel.

We need to set jsx to react. The JSX parameter indicates the behavior the TypeScript compiler should use to handle the embedded JSX syntax found in .tsx files. In this case, we want the JSX syntax to be emitted as React directives.

We don't want the TypeScript compiler output to be written alongside the original files in the src directory, so we'll use the outDir parameter to redirect them to a directory called artifacts.

We set the filesGlob array to indicate the files that the compiler should consume. These include the type definition files downloaded by typings, as well as any .ts or .tsx files in the src directory.

Finally, we'll exclude the node_modules and artifacts directory.

{
    "compilerOptions": {
        "module": "commonjs",
        "target": "es6",
        "jsx": "react",
        "outDir": "artifacts",
        "sourceMap": false
    },
    "filesGlob": [
        "typings/index.d.ts",
        "src/**/*.ts",
        "src/**/*.tsx"
    ],
    "exclude": [
        "node_modules",
        "artifacts"
    ]
}

VS Code - Workspace settings

We want VS Code to use the specific version of the TypeScript compiler we installed with npm as defined in package.json. To do so, we need to add the following to the workspace settings at .vscode/settings.json:

{
    "typescript.tsdk": "node_modules/typescript/lib"
}

VS Code - Build task

Although the project can be built at the command line using tsc -p . to invoke the TypeScript project compiler on the current directory, it is more convenient to do so from VS Code.

After adding the following to .vscode/tasks.json, the ⇧⌘B shortcut can be used to invoke the compiler. Any errors will be shown in the VS Code problems console.

{
    // See https://go.microsoft.com/fwlink/?LinkId=733558
    // for the documentation about the tasks.json format
    "version": "0.1.0",
    "windows": {
        "command": ".\\node_modules\\.bin\\tsc"
    },
    "linux": {
        "command": "./node_modules/.bin/tsc"
    },
    "osx": {
        "command": "./node_modules/.bin/tsc"
    },
    "isShellCommand": true,
    "args": ["-p", "."],
    "showOutput": "silent",
    "problemMatcher": "$tsc"
}

Hello World!

To check that the compiler is set up properly, we'll create a small TypeScript file and ensure there are no errors.

Create src/main.ts with the following:

console.log("Hello World!");  

Now compile the project using tsc -p . or ⇧⌘B. If everything went well, there should be no errors and a file should have been output at artifacts/main.js.

See the diff for this step on GitHub

Set up TSLint

If you would like to lint your TypeScript files, you can add TSLint:

$ npm install tslint --save
$ tslint --init

You should now have a tslint.json file with a default set of rules you can customize.

If you are using VS Code, you'll want to add the following to the workspace settings at .vscode/settings.json to ensure the IDE uses your custom rules file:

{
  "tslint.configFile": "tslint.json"
}

To test whether linting is working properly, add eval("") to main.ts. You should immediately see a linter error in the VS Code problems console, as well as when executing tslint -c tslint.json --project tsconfig.json from the command line.

See the diff for this step on GitHub

Create .tsx files

The default React Native project has two main code entry points, one for iOS and one for Android:

  • index.ios.js
  • index.android.js

Both of these files are located in the root of the project. However, we want to convert these to TypeScript and relocate them to the src directory.

React Native has several build scripts and native code files that reference these two files by default. Rather than edit all of these locations, we'll replace the default files above with a chain loader that loads our TypeScript artifacts.

To do so, we first move these two files to the src directory and rename them to .tsx (ignoring the compilation errors for now).

$ mv index.ios.js ./src/root.ios.tsx
$ mv index.android.js ./src/root.android.tsx

Next, we'll create two new files where the old ones used to be with a simple require statement to load the TypeScript artifacts.

// New index.ios.js
require("./artifacts/root.ios.js");

// New index.android.js
require("./artifacts/root.android.js");  

Now, when React Native loads, it will still load the index.ios.js or index.android.js files from their original locations. These new files will load our TypeScript compiled files, which will be located in the artifacts directory.

See the diff for this step on GitHub

Fix compilation errors

Now we need to fix the compilation errors in the .tsx files created in the last step.

First, the import statements need to be modified slightly.

import * as React from "react";  
import { Component } from "react";

import {  
    AppRegistry,
    StyleSheet,
    Text,
    View
} from "react-native";

Second, we need to update the class declaration to include types for the properties and state objects. In this case we'll just include any, since we don't have either in this sample view.

class SampleApp1 extends Component<any, any> {  
...
}

Finally, we need to update the style declarations to include their types by using the as keyword.

const styles = StyleSheet.create({  
  container: {
    flex: 1,
    justifyContent: "center",
    alignItems: "center",
    backgroundColor: "#F5FCFF",
  } as __React.ViewStyle,

  welcome: {
    fontSize: 20,
    textAlign: "center",
    margin: 10,
  } as __React.TextStyle,

  instructions: {
    textAlign: "center",
    color: "#333333",
    marginBottom: 5,
  } as __React.TextStyle,
});

Now we compile the project using tsc -p . or ⇧⌘B. We should see two new files output into the artifacts directory.

  • artifacts/root.ios.js
  • artifacts/root.android.js

At this point, you should be able to launch your app onto a device and/or simulator using react-native run-ios.

See the diff for this step on GitHub

Fix TSLint errors

Now we need to go back and address the compilation errors in the .tsx files we just created. The TSLint defaults want double quotes for strings instead of single quotes. You can either make that change, or adjust your tslint.json file.

See the diff for this step on GitHub

Build your app!

At this point you should have TypeScript up and running with React Native.

If you are building a brand new application, consider setting noImplicitAny to true in your tsconfig.json file, to help enforce using type declarations everywhere.

If you missed a step, or just want to clone this sample repository to start your own app, check it out on GitHub!