React Native Expo+TypeScript超入門

## はじめに
この記事は [React Native Advent Calendar 2018](https://qiita.com/advent-calendar/2018/react-native) 21日目の記事です。

[Expo v31](https://blog.expo.io/expo-sdk-v31-0-0-is-now-available-cad6d0463f49)でBabel7がサポートされたことに伴って、TypeScriptもサポートされることになりました。
これによりExpo+TypeScriptが楽にできるようになったので、その入門記事です。

対象読者としては、今までExpoで軽く開発してたけどTypeScriptも使ってみたくなった初心者 (つまりちょっと前の私) みたいな人に向けて書いてます。
この記事を読めばなんとなくExpo+TSの概要は掴めるかな？ という感じ。
Expo+TSの導入記事は既にたくさんあるので詳細はそちらを参考にしていただけると。

Expoの動きについては[11日目のゆうさんの記事](https://qiita.com/watanabe_yu/items/f489f08c630108a7ab7e)とか

## TypeScript
とりあえず入れておきましょう

```bash
$ npm install -g typescript
```

### 概要
- Microsoftが開発したAltJS  (みんな大好きVSCodeと相性がいいよ！)
- 静的型付けとクラスベースのオブジェクト指向
- JSの大規模開発に対応するために設計
- JSのスーパーセット(上位互換)で コンパイルすることでJSにトランスパイルできる

### 何が嬉しいのか
- コンパイルで事前に怒られるのでエラーが一目瞭然 (JSのエラー追うの辛い)
- コードの影響範囲の抽出が容易なため変更に強い
- 型が付くことでメソッド・変数の意図がわかりやすい
- 型と付くことと、自然と行儀の良いコードになることで可読性向上
- エディタの補完機能が強い (VSCode)
- 型推論によって初期値の型を採用してくれるため慣れてなくても書きやすい

### 基本の型
公式：[Basic Types](https://www.typescriptlang.org/docs/handbook/basic-types.html)

```ts
// Boolean
let isDone: boolean = false;
// Number
let decimal: number = 6;
let hex: number = 0xf00d;
// String
let color: string = "blue";
color = 'red';
// Array
let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3]; // generic
// Tuple
let x: [string, number];
x = ["hello", 10]; // OK
x = [10, "hello"]; // Error
// Enum
enum Color {Red, Green, Blue}
let c: Color = Color.Green;
// Any
let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; 
// Void
function warnUser(): void {
    console.log("This is my warning message");
}
```

### interface
公式：[Interfaces](https://www.typescriptlang.org/docs/handbook/interfaces.html)

```ts
// 基本はこんな感じ
interface Person {
    firstName: string;
    lastName: string;
}

function greeter(person: Person) {
    return "Hello, " + person.firstName + " " + person.lastName;
}

let user = { firstName: "Jane", lastName: "User" };

// 任意のプロパティには？
interface SquareConfig {
    color?: string;
    width?: number;
}

// 読み込み専用
interface Point {
    readonly x: number;
    readonly y: number;
}

// Function (string型の引数を二つとってbooleanを返す関数)
interface SearchFunc {
    (source: string, subString: string): boolean;
}

// Class
interface ClockInterface {
    currentTime: Date;
}

class Clock implements ClockInterface {
    currentTime: Date;
    constructor(h: number, m: number) { }
}

// interfaceの拡張
interface Shape {
    color: string;
}

interface Square extends Shape {
    sideLength: number;
}

let square = <Square>{};
square.color = "blue";
square.sideLength = 10;
```

## TypeScript with React Native

### 概要
- 最初はとりあえずBasic Typesとinterfaceがわかっていれば大丈夫そう
- propsやstateをinterfaceとして定義しておくイメージ
- `npm install`で出てくる`@types/hoge`というのは`hoge`パッケージの型定義ファイル
- [Microsoft/TypeScript-React-Native-Starter](https://github.com/Microsoft/TypeScript-React-Native-Starter)をさらっと見ておけばなんとなく掴めそう

```tsx
import React from "react"
import { Button, StyleSheet, Text, View } from "react-native"

export interface Props {
  name: string
  enthusiasmLevel?: number
}

interface State {
  enthusiasmLevel: number
}

export class Hello extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props)

    if ((props.enthusiasmLevel || 0) <= 0) {
      throw new Error("You could be a little more enthusiastic. :D")
    }

    this.state = {
      enthusiasmLevel: props.enthusiasmLevel || 1
    }
  }

  onIncrement = () => this.setState({ enthusiasmLevel: this.state.enthusiasmLevel + 1 });
  onDecrement = () => this.setState({ enthusiasmLevel: this.state.enthusiasmLevel - 1 });
  getExclamationMarks = (numChars: number) => Array(numChars + 1).join("!")

  render() {
    return (
      <View style={styles.root}>
        <Text style={styles.greeting}>
          Hello {this.props.name + this.getExclamationMarks(this.state.enthusiasmLevel)}
        </Text>
        
        <View style={styles.buttons}>
          <View style={styles.button}>
            <Button
              title="-"
              onPress={this.onDecrement}
              accessibilityLabel="decrement"
              color="red"
            />
          </View>

          <View style={styles.button}>
            <Button
              title="+"
              onPress={this.onIncrement}
              accessibilityLabel="increment"
              color="blue"
            />
          </View>
        </View>
      </View>
    )
  }
}

// styles割愛
```
TSが絡む部分のざっくりとした説明としては、

- `Hello`コンポーネントは、interfaceで定義されたstring型の`name`とnumber型の任意の`enthusiasmLevel`という`props`とnumber型の`enthusiasmLevel`という`state`を持つ
- `getExclamationMarks`という関数はnumber型の`numChars`という引数を持つ

### 実際に導入してみる

#### 普通のExpoアプリを作る

```bash
$ expo init my-ts-app
$ cd my-ts-app
$ npm start
```

<img width="351" alt="スクリーンショット 2018-12-21 0.14.05.png" src="https://qiita-image-store.s3.amazonaws.com/0/196629/ff09e80c-4b7c-4ba6-c834-a8b3eebd188e.png">

いつもの画面ですね

#### TypeScriptを導入
サポートされているので`npm`で入れるだけです。

```bash
$ npm install --save-dev typescript

// tsconfig.jsonの作成
$ tsc --init

// とりあえずtsxにしてみる
$ mv App.js App.tsx
$ expo start
```

ちゃんと動いてるっぽいので色々入れていきます。

型定義ファイルとか

```bash
$ npm install --save-dev @types/react @types/react-native @types/expo
```

こんなことを言われるので`tsconfig.json`を編集。
元々設定が書いてあると思うので、そのコメントアウトを外します。

```tsconfig.json
- //"jsx": "preserve",
+ "jsx": "preserve",
```

#### TypeScriptで書いてみる
せっかくなので、先程の[Microsoft/TypeScript-React-Native-Starter](https://github.com/Microsoft/TypeScript-React-Native-Starter)っぽいものを書いて動かしてみます。

```App.tsx
import React from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Hello from "./Hello"

type Props = {};

export default class App extends React.Component<Props> {
  render() {
    return (
      <View style={styles.container}>
        <Text>My Expo + TypeScript App!</Text>
        <Hello name="Expo" enthusiasmLevel={10} />
      </View>
    );
  }
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    backgroundColor: '#fff',
    alignItems: 'center',
    justifyContent: 'center',
  },
});
```

```Hello.tsx
import React from "react";
import { Button, StyleSheet, Text, View } from "react-native";

interface Props {
  name: string;
  enthusiasmLevel?: number;
}

interface State {
  enthusiasmLevel: number;
}

class Hello extends React.Component<Props, State> {
  constructor(props: Props) {
    super(props);

    if ((props.enthusiasmLevel || 0) <= 0) {
      throw new Error("You could be a little more enthusiastic. :D");
    }

    this.state = {
      enthusiasmLevel: props.enthusiasmLevel || 1
    };
  }

  onIncrement = () =>
    this.setState({ enthusiasmLevel: this.state.enthusiasmLevel + 1 });
  onDecrement = () =>
    this.setState({ enthusiasmLevel: this.state.enthusiasmLevel - 1 });
  getExclamationMarks = (numChars: number) => Array(numChars + 1).join("!");

  render() {
    return (
      <View style={styles.root}>
        <Text style={styles.greeting}>
          Hello{" "}
          {this.props.name +
            this.getExclamationMarks(this.state.enthusiasmLevel)}
        </Text>

        <View style={styles.buttons}>
          <Button title="decrement" onPress={this.onDecrement} color="red" />
          <Button title="increment" onPress={this.onIncrement} color="blue" />
        </View>
      </View>
    );
  }
}

export default Hello;

const styles = StyleSheet.create({
  root: {
    alignItems: "center",
    alignSelf: "center"
  },
  buttons: {
    flexDirection: "row",
    alignItems: "center"
  },
  button: {
    fontSize: 20
  },
  greeting: {
    color: "gray",
    fontWeight: "bold",
    fontSize: 30
  }
});
```
![1ffc6b1001dd5a46e26d92a88d963344.gif](https://qiita-image-store.s3.amazonaws.com/0/196629/6fdb4082-0ea5-8120-f08e-4de79575498d.gif)
無事動きました！！！

ちなみにpropsの型を間違えるとちゃんと怒ってくれます。
<img width="796" alt="スクリーンショット 2018-12-20 10.55.03.png" src="https://qiita-image-store.s3.amazonaws.com/0/196629/6ee3cfa2-3d60-040c-b337-5487cc9b0c57.png">


あとは環境に合わせて`tsconfig.json`を設定したりしながらExpo+TypeScriptで開発しましょう！

## 参考
- https://blog.expo.io/expo-sdk-v31-0-0-is-now-available-cad6d0463f49
- https://www.typescriptlang.org/index.html
- https://github.com/Microsoft/TypeScript-React-Native-Starter
- http://otiai10.hatenablog.com/entry/2018/11/21/152730