2019-03-06

Create React Native App using TypeScript with Babel and Expo

(updated 2019-07-23 for clarity)

Here's every step to creating a React Native app (with or without Expo), written in and type checked with TypeScript, but compiled with Babel 7.

You're assumed to have some proficiency with using the terminal.

Tools to Install on your PC

I'm on a Mac, but the steps shouldn't be much different on Ubuntu Linux or Windows.
  1. Install Homebrew.
    It's a package manager for Macs, and Linux too, but if you're on Ubuntu or Debian, I suggest just use the built-in one like APT.
  2. Install Node thru brew.
    Installing Node should include the NPM package manager with it, which is needed below.
  3. Some say to next install Yarn thru brew.
    Yarn is yet another node package manager... you can skip this if you want, it's not strictly needed, and I won't use Yarn in this tutorial.
  4. Fix NPM if you installed Yarn.
    I actually mentioned Yarn at all only because with Homebrew, you might now need to fix the Node NPM install as it might be broken by installing yarn.  Simply[1] run: yarn global add npm.  Remember, we don't need Yarn in this tutorial though.
  5. Install Expo thru npm:npm install -g expo-cli

    Expo is apparently kind of a mini-platform for running React Native apps, and makes it easy to test a React Native app on devices through its Expo app, or on Macs and PCs without resorting to an Android or iOS emulator (which is great, because those emulators are dreadfully slow in my experience).  Plus installing Expo should include React Native with it.
  6. Install React Native CLI thru npm:npm install -g react-native-cli
    This is basically a template that creates a RN project for you to start from.
[1] https://stackoverflow.com/questions/33870520/npm-install-cannot-find-module-semver/49422151#49422151

New Project with React Native or Expo

1. Create a React Native project (possibly thru Expo):
$ expo init CoolProject
or alternatively without Expo:
$ react-native init CoolProject

1.5. Upgrade core-js:
I got an error like this:
warning react-native > create-react-class > fbjs > core-js@1.2.7: core-js@<2.6.8 is no longer maintained. Please, upgrade to core-js@3 or at least to actual version of core-js@2.

You just need to upgrade core-js.  Go into your CoolProject folder and run:
npm install --save core-js@^3

2. cd CoolProject to go into the new project's root directory, then install Babel:
$ npm install --save-dev @babel/core @babel/cli
I assume this will install for you Babel 7 or above.  We'll need it later to migrate to TypeScript.

3. Create a lib folder in the project root.
$ mkdir lib
The lib folder will be used to contain the App's JavaScript files. Traditionally, the folder would be called "src", but looking forward, these JavaScript files eventually will be produced by the TypeScript compiler for us.

So instead, we're going to reserve "src" for later when we migrate to TypeScript, instead of calling it "src" right now when we're still dealing with just JavaScript JS, or JSX, files.

Separating the TypeScript and the compiled JavaScript files is a technique to avoid in-source builds (a usual technique used in compiled languages like C++: see e.g. in-source vs out-of-source builds).


4. Adjust the project's entry point to running your code.
Prior to creating the lib folder to contain the App's JavaScript files, React Native projects would go looking for your programming code via an index.js, or App.js file.  This needs an adjustment given we want to contain everything in the lib directory.

Expo React Native apps are slightly different than plain React Native apps though:
  • Expo has no platform native specific code, so there's no index.js file to speak of. Instead App.js is the entry point and it's essentially hard coded into the Expo framework.  So instead[2], create a Main.js file in lib and have App.js essentially point there.
    • Thus App.js should look like this:
import React from 'react';
import Main from './lib/Main';

const App = () => (
  <main />
);

export default App;
    • And lib/Main.js is like:
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';

export default class Main extends React.Component {
  render() {
    return (
      <View style={styles.container}>
        <Text>Modify me!</Text>
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
  • For plain React Native projects (i.e. created with react-native init CoolProj instead of the expo equivalent), there should be an index.js file.  In it there's code to register App.js as the non-native cross-platform JavaScript entry point.  Just move the existing App.js file into the lib folder and modify index.js to point to ./lib/App.js instead.

Migrate to TypeScript

I assume Babel 7 or above was installed already, because it means we can just use Babel to compile the TypeScript for us.  We'll use the actual TypeScript compiler purely for type checking later.

1. Install the Babel TypeScript presets, etc:

From within the project's root directory:
$ npm install --save-dev @babel/preset-typescript @babel/preset-env @babel/plugin-proposal-class-properties @babel/plugin-proposal-object-rest-spread

2. Set up .babelrc in the project root with at least the following in it:
{
    "presets": [
        "@babel/env",
        "@babel/preset-typescript"
    ],
    "plugins": [
        "@babel/proposal-class-properties",
        "@babel/proposal-object-rest-spread"
    ]
}

3. Change the lib folder's name to src.

4. Migrate JavaScript files to TypeScript file extensions

Go into the src folder and change all the file extensions from .jsx to .tsx, and from .js to .ts.

5. Compile your project:
$ npx babel ./src --out-dir lib --extensions ".ts,.tsx"
You should now have a lib directory of JavaScript files compiled from the TypeScript files from the src folder.

Because we started with a lib folder previously, prior to migrating to TypeScript, there's now no need to modify App.js or index.js to point to lib, since it was pointing there all along (nice bit of foresight)!

6. Make the compilation command into a script:

In the package.json file, add the following "compile" line:
...
  "scripts": {
...
    "compile": "babel ./src --out-dir lib --extensions '.ts,.tsx'"
...}

Now you can easily compile using: npm run compile

Caveat - there's 4 TypeScript features that Babel won't compile:

  1. namespaces:  Just use standard ES6 modules (import / export) instead
  2. Type casting like <NewType>x when JSX is enabled anywhere.  Just write x as NewType instead.
  3. enums that span multiple declarations. So not open ended then.
  4. legacy-style import/export syntax: e.g. import foo = require(...) or export = foo

Setup TypeScript for Type Checking

1. Install TypeScript compiler.
$ npm install --save-dev typescript

2. Install TypeScript type definitions for React Native

Install[3] type definitions and declarations for React, the test framework Jest, etc.
$ npm install --save-dev @types/jest @types/react @types/react-native @types/react-test-renderer

3. Set up tsconfig.json in the project root with at least the following in it:
{
  "compilerOptions": {
    "target": "esnext",
    "moduleResolution": "node",
    "allowJs": true,
    "noEmit": true,
    "strict": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "jsx": "react-native"
  },
  "include": [
    "./src"
  ],
  "exclude": [
      "./node_modules"
  ]
}

There's lots of compiler options.  Read more about "jsx" and "allowSyntheticDefaultImports" if you want.

4. Try type checking the project by running npx tsc. Fix duplicate types error if needed.

If there's no errors, then great!  I got a lot of duplicate type definition errors though.  It's because there are duplicate types defined for React Native conflicting with the default definitions provided by TypeScript's definition files for the dom etc.

There's two ways to fix this duplicate types error:
  • (1)  The better, preferable option in my mind is to go in tsconfig.json, and add this "lib" compiler option:
{
  "compilerOptions": {
  ...
    "lib": ["esnext"],
  ...
  }
}

The "lib" compiler option by default also include the dom type definitions from the TypeScript compiler. Specifying it here like so means it won't be included, thus won't conflict with the React Native type definitions (according to Typescript: Duplicated identifier between React Native and ES6 Lib).
  • (2)  A much more drastic, draconian way to fix it is to go inside tsconfig.json, and add the "skipLibCheck" compiler option:
{
  "compilerOptions": {
  ...
    "skipLibCheck": true
  ...
  }
}
It's draconian because this option makes the TypeScript compiler skip type checking all type declaration files.  It's kind of a nuclear option to fixing the problem (see error TS2300: Duplicate identifier 'RequestInfo' Ask)

5. Make the type checking command into a script:

In the package.json file, add the following "tsc" line:
...
  "scripts": {
...
    "tsc": "npx tsc"
...}

Now you can use TypeScript compiler to check types by either npx tsc or with npm run tsc.

6. Adjust type checking options to taste:

See: Unofficial React Native TypeScript

More TypeScript compiler options: TypeScript docs: Compiler Options

Future Work

Use a test framework, like Jest.

Learn more Mobile App Development with React Native (CSCI E-39b, produced by CS50)

References

I learned from many others, but oddly most only write on one aspect or another but don't put the whole thing together from top to bottom like I did above.  Thanks to the following though!

[3] TypeScript With Babel: A Beautiful Marriage

[4] Microsoft/TypeScript-React-Native-Starter

[5] React Native Blog: Using TypeScript with React Native
Note this seems like a full copy pasta of the above Microsoft/TypeScript-React-Native-Starter read me page, or vice versa, or written by the same person, or... I gave up trying to figure that out.  But the blog post was awesome in pointing out the new TypeScript and Babel 7 support.

[6] Microsoft TypeScript blog: TypeScript and Babel 7

[7] React Native Docs: Getting Started

No comments: