23 July 2019

Create React Native App using TypeScript compiled with Babel


Here's every step to creating a React Native app, 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 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

1. Create a React Native project:
$ 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.
For React Native projects initially created from its RN template (i.e. created with react-native init CoolProj ), 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 instead.

In the latest version of the template (i.e. as of 2019 July 23), the App.js file is actually a JSX file.  So in addition, change the App.js filename to App.jsx --- this is important for the migration to TypeScript next.

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
Maybe there's some type errors?  Most of mine are duplicate types error.  Here's how to fix it.

4.1. 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)

4.2 Fix missing global
After fixing duplicate types errors, I only had 2 more errors to fix.

The newest RN template App.js uses global, which gave me this error:
src/App.tsx:36:12 - error TS2304: Cannot find name 'global'.

A quick way to fix it[8] is to declare it yourself, so before the function using global, just add:
declare var global: any;

4.3 Missing declaration file for module 'react-native/Libraries/NewAppScreen'
The last error is just that the RN template uses a module with no TypeScript types declared:
src/App.tsx:25:8 - error TS7016: Could not find a declaration file for module 'react-native/Libraries/NewAppScreen'.


There's two ways to fix it:

(1) I believe the easy way to fix it is to just ignore it.  Babel "compiles" TypeScript by ignoring types anyway --- call it type erasure if you like.  We're only using the TypeScript compiler tsc to do type checking, not actually compiling with it.  So all types are, in effect, optional.

Eventually you'll replace it anyway as it's just a template for you to build from, right?

(2) The more type correct and annoying fix is to replace it with another RN view that does have type declarations[9].  Go into App.tsx and replace each NewAppScreen with e.g. <View style={{flex:1,backgroundColor:'orange'}}>

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

[8] React Native with TypeScript: How to set up globals in setup.js?

[9] https://stackoverflow.com/a/56892669

No comments: