TypeScript
reactnative

React Native v0.57がTypeScriptに対応したらしいので動かしてみた

v0.57でいろいろ変わるという記事を書きましたが、本当にMetro BundlerがTypeScriptを食べてくれるのか半信半疑だったので、実際に試してみました。

結論

ビルドだけならゼロコンフィグに近い感じで行けました。型チェックをする場合はMSのブログでのやり方よりも、もうひと工夫必要です。

サンプルコード

v0.57.0-rc.3でプロジェクトを作ってみました。これをベースにお話します。

https://github.com/Nkzn/ReactNative057Sample

前提条件:Babel 7のTypeScript対応の仕組み

React NativeのTypeScript対応は、Babel 7のTypeScript対応に乗っかった形で実現されています。

ここで重要なのは、 Babelはtscを内部で実行するわけではない ということです。

今回導入された@babel/preset-typescriptは、FlowTypeの babel-plugin-flow-strip-types と同様に、TypeScript/FlowからTypeScript/Flow特有の文法を引き剥がせば、Babelでパース可能なJSになる という発想で作られています。

@babel/preset-typescriptは、古いJavaScriptに変換することに興味がありません。それはBabelの領分です。

@babel/preset-typescriptは、型チェックに興味がありません。文法を引き剥がすことしかできません。

型チェックをしたい場合は、tsconfig.jsonに特殊な設定をして、tscをflowと同様に型チェックしかできないコマンドにすることで実現します。

という前提で、実際の手順をどうぞ。

とりあえずinit

$ react-native init RN057Sample --version 0.57.0-rc.3

普通のプロジェクトができます。

RN057Sample/
  - App.js
  - index.js
  - などなど

node_modulesの中を覗いたら、metro付近にTypeScriptの文字が見えて、この時点でちょっとテンションが上がります。

雑にtsxにする

型チェックを気にしなければ、tsxファイルさえあれば行けるはずなので、App.jsをApp.tsxにリネームします。

RN057Sample/
  - App.tsx
  - index.js
  - などなど

ついでにimportをTypeScript的に問題ないように書き換えます。

App.jsをApp.tsxに書き換え
  /**
  * Sample React Native App
  * https://github.com/facebook/react-native
  *
- * @flow
  * @format
  */

- import React, {Component} from 'react';
+ import React from 'react';
  import {Platform, StyleSheet, Text, View} from 'react-native';

  const instructions = Platform.select({
    ios: 'Press Cmd+R to reload,\n' + 'Cmd+D or shake for dev menu',
    android:
      'Double tap R on your keyboard to reload,\n' +
      'Shake or press menu button for dev menu',
  });

  type Props = {};
- export default class App extends Component<Props> {
+ export default class App extends React.Component<Props> {
    render() {
      return (
        <View style={styles.container}>
          <Text style={styles.welcome}>Welcome to React Native!</Text>
          <Text style={styles.instructions}>To get started, edit App.js</Text>
          <Text style={styles.instructions}>{instructions}</Text>
        </View>
      );
    }
  }

  const styles = /* 省略 */

はい、ほとんど変えてませんね。type Props = {};もこのくらいならFlowとTypeScriptの文法は同じですし、そのままです。

では、この状態で動かしてみましょう。 react-native run-ios です。

動いちゃった

スクリーンショット 2018-08-30 16.13.18.png

HAHAHA、動いちゃったよHAHAHA! :laughing:

ひとまず動かすだけなら、このくらいの気軽さでいけます。ただ、tsconfig.jsonを置いてないのもあって、このままではVisual Studio Code等の動きはよくないです。JSに毛が生えた程度のやる気の無さです。やはり、ちゃんとTypeScriptらしい設定をしたほうがよさそうです。

型チェックを入れてみよう

それでは次は、型チェックをしてみましょう。型定義ファイルとtscを導入します。

$ yarn add typescript @types/react @types/react-native --dev
or
$ npm install typescript @types/react @types/react-native --save-dev

次にtsconfig.jsonを作ります。MSの記事では次のようになっています。

tsconfig.json
{
  "compilerOptions": {
    // Target latest version of ECMAScript.
    "target": "esnext",
    // Search under node_modules for non-relative imports.
    "moduleResolution": "node",
    // Process & infer types from .js files.
    "allowJs": true,
    // Don't emit; allow Babel to transform files.
    "noEmit": true,
    // Enable strictest settings like strictNullChecks & noImplicitAny.
    "strict": true,
    // Disallow features that require cross-file information for emit.
    "isolatedModules": true,
    // Import non-ES modules as default imports.
    "esModuleInterop": true
  },
  "include": [
    "src"
  ]
}

include設定で、src/の中だけをチェックするようになっています。良い機会ですのでApp.tsxはsrcの中に入れておきましょう。

RN057Sample/
  - src/
      - App.tsx
  - index.js
  - tsconfig.json
  - などなど

では、この状態でtscを実行してみましょう。

$ npx tsc
node_modules/@types/react-native/globals.d.ts:36:15 - error TS2300: Duplicate identifier 'FormData'.

36 declare class FormData {
                 ~~~~~~~~

  node_modules/typescript/lib/lib.dom.d.ts:5367:11
    5367 interface FormData {
                   ~~~~~~~~
    'FormData' was also declared here.

node_modules/@types/react-native/globals.d.ts:66:3 - error TS2717: Subsequent property declarations must have the same type.  Property 'body' must be of type 'string | ArrayBuffer | ArrayBufferView | Blob | FormData| URLSearchParams | ReadableStream | null | undefined', but here has type 'string | ArrayBuffer | DataView | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | Blob | FormData | null | undefined'.

66   body?: BodyInit_;
     ~~~~

node_modules/@types/react-native/globals.d.ts:92:14 - error TS2300: Duplicate identifier 'RequestInfo'.

92 declare type RequestInfo = Request | string;
                ~~~~~~~~~~~

  node_modules/typescript/lib/lib.dom.d.ts:17155:6
    17155 type RequestInfo = Request | string;
               ~~~~~~~~~~~
    'RequestInfo' was also declared here.

node_modules/@types/react-native/globals.d.ts:111:13 - error TS2403: Subsequent variable declarations must have the same type.  Variable 'Response' must be of type '{ new (body?: string | ArrayBuffer | ArrayBufferView | Blob | FormData | URLSearchParams | ReadableStream | null | undefined, init?: ResponseInit | undefined): Response; prototype: Response; error(): Response; redirect(url: string, status?: number | undefined): Response; }', but here has type '{ new (body?: string | ArrayBuffer | DataView | Int8Array | Uint8Array | Uint8ClampedArray | Int16Array | Uint16Array | Int32Array | Uint32Array | Float32Array | Float64Array | Blob | FormData | null | undefined, init?: ResponseInit | undefined): Response; prototype: Response; error: () => Response; redirect: (url: ...'.

111 declare var Response: {
                ~~~~~~~~

めっちゃエラーが出ます。React Native環境でそのまま動くわけではないんですね。

React Native向けのtsconfig.jsonを構成する

ひとまず、前述のようなエラーはtsconfig.jsonにlibを設定すれば動く経験則がある(適当)ので、次のように設定します。

tsconfig.json
  {
    "compilerOptions": {
      // from https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/
      "target": "esnext",
      "moduleResolution": "node",
      "allowJs": true,
      "noEmit": true,
      "strict": true,
      "isolatedModules": true,
      "esModuleInterop": true,
+     // additional
+     "lib": ["esnext"]
    },
    "include": [
      "src"
    ]
  }

esnextにしましたが、なんとなくtargetに揃えただけなので、React Nativeで使える文法と齟齬がありそうな気はしなくもないです。es2017くらいのほうが無難かも。

これでどうでしょうか。npx tscしてみます。

$ npx tsc
src/App.tsx:8:8 - error TS1192: Module '"/path/to/RN057Sample/node_modules/@types/react/index"' has no default export.

8 import React from 'react';
         ~~~~~

src/App.tsx:10:20 - error TS6142: Module './MyText' was resolved to '/path/to/RN057Sample/src/MyText.tsx', but '--jsx' is not set.

10 import MyText from "./MyText";
                      ~~~~~~~~~~

src/App.tsx:23:7 - error TS17004: Cannot use JSX unless the '--jsx' flag is provided.

23       <View style={styles.container}>
         ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/App.tsx:24:9 - error TS17004: Cannot use JSX unless the '--jsx' flag is provided.

24         <Text style={styles.welcome}>Welcome to React Native!</Text>
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/App.tsx:25:9 - error TS17004: Cannot use JSX unless the '--jsx' flag is provided.

25         <Text style={styles.instructions}>To get started, edit App.js</Text>
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/App.tsx:26:9 - error TS17004: Cannot use JSX unless the '--jsx' flag is provided.

26         <Text style={styles.instructions}>{instructions}</Text>
           ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

src/MyText.tsx:1:8 - error TS1192: Module '"/path/to/RN057Sample/node_modules/@types/react/index"' has no default export.

1 import React from "react";
         ~~~~~

src/MyText.tsx:5:5 - error TS17004: Cannot use JSX unless the '--jsx' flag is provided.

5     <Text>{props.text}</Text>
      ~~~~~~

まだダメっぽい。でもここまでくれば割と簡単です。

import React from 'react';の問題については、RN公式ブログのTypeScript記事で言及がある通り、tsconfig.jsonにallowSyntheticDefaultImportsを設定してあげれば解決です。

また、JSX周りのエラーはメッセージの通り、tsconfig.jsonにjsxを設定してあげれば解決しそうです。

tsconfig.jsonを次のようにしてみましょう。

tsconfig.json
      // additional
+     "allowSyntheticDefaultImports": true,
+     "jsx": "react",
      "lib": ["esnext"]

これで npx tsc を実行すると、バッチリ成功します。というわけで、tsconfig.jsonの最終形は次のようになります。

tsconfig.json
{
  "compilerOptions": {
    // from https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/
    "target": "esnext",
    "moduleResolution": "node",
    "allowJs": true,
    "noEmit": true,
    "strict": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    // additional
    "allowSyntheticDefaultImports": true,
    "jsx": "react",
    "lib": ["esnext"]
  },
  "include": [
    "src"
  ]
}

Hello, TypeScript!

まとめ

長々と書いてきましたが、実は作業としては3つだけです。

  1. 拡張子を.tsx(または.ts)にする
  2. TypeScriptや型定義ファイルをdevDependenciesに追加する
  3. tsconfig.jsonを設定する

これだけで、React Nativeアプリ開発にTypeScriptを導入することができるようになります。

tscをコンパイラではなくType Checkerとして使うのは少し抵抗がありますが、これはRN独自のやり方ではなく、Babel界隈で今後スタンダードになりうる方法なので、腰を据えて慣れていったほうがよいのでしょうね。

そんなわけで、v0.57が待ち遠しいです。