ReactのプロジェクトにTypeScriptを導入する〜npm installからコンパイルまで〜

# はじめに

TypeScript Advent Calendarの初日ということで、TypeScriptの導入についての記事を書いてみました。
**主に「いまReactを使っていて、TypeScriptも使ってみたい！」という方に向けた記事です。**

最初にTypeScriptについて触れておくと、
TypeScriptは「型を持ったJavaScriptのスーパーセットです」

[TypeScriptの公式サイト](https://www.typescriptlang.org/)
[TypeScriptのリポジトリ](https://github.com/Microsoft/TypeScript)

- Microsoftによって開発され、メンテナンスされているオープンソースのプログラミング言語
- クラスベースのオブジェクト指向
- 型情報をもとに実行前にエラーを検出でき、コンパイルする事でJavaScriptのコードにトランスパイルされる
- ES2015移行で書かれたコードをES5にトランスパイルできる
- JSXをサポートしているので、Reactとも併用できる

という特徴を持っています。

Google Trendsを見ても、その人気が分かると思います。
<img width="1274" alt="typescript-trends.png" src="https://qiita-image-store.s3.amazonaws.com/0/107472/af31c77e-ed4e-50a9-764d-dad33a51abf0.png">
*[TypeScriptのトレンド動向](
https://trends.google.co.jp/trends/explore?date=today%205-y&q=typescript)*


それではReactのプロジェクトにTypeScriptを導入する流れを紹介します。

# 導入するReactのプロジェクト
今回、Reactで書かれたカウンターアプリをサンプルとして使います。
サンプルプロジェクトのリポジトリは[こちら](https://github.com/toshi-toma/typescript-with-react-example/tree/React)を参照ください。

プロジェクトの構成は以下の通りです。

- index.jsを起点にしたReactのコンポーネント
- トランスパイルやコンパイルはwebpack 4 + Babel 7

```bash
$ tree                                                                                                                                               (master)
.
├── dist
│   ├── bundle.js
│   └── index.html
├── package.json
├── .babelrc
├── src
│   ├── index.html
│   ├── index.js
│   └── js
│       └── components
│           ├── container
│           │   └── CounterContainer.js
│           └── presentational
│               └── Button.js
└── webpack.config.js
```

# TypeScriptの導入

それでは紹介したカウンターアプリにTypeScriptを導入します。

※ 新規でTypeScriptの環境を構築する場合は[create-react-app](https://github.com/facebook/create-react-app)で簡単に行えます。

今回変更した内容は[このプルリクエスト](https://github.com/toshi-toma/typescript-with-react-example/pull/1/files)で確認できます。

TypeScriptを用いたReactのファイルは拡張子が`.tsx`になります。
なので、今回は「Reactのファイルが全て`.tsx`になり、コンパイルが通る」ことがゴールです。

## 1. TypeScriptのインストール
まずはTypeScriptに必要なライブラリをインストールします。

```bash
$ npm i typescript ts-loader --save-dev
```
ts-loaderは、webpackの中でTypeScriptで書かれたファイルをjsファイルに変換するためにインストールします。

## 2. webpackの設定(webpack.config.js)
webpackの設定をTypeScript(tsx)用に変更します。
webpack.config.jsの変更内容は以下の通りです。

```diff
module.exports = {
- entry: './src/index.js',
+ entry: './src/index.tsx',
  ...
  resolve: {
-   extensions: ['*', '.js', '.jsx']
+   extensions: ['.ts', '.tsx', '.js']
  },
  module: {
    rules: [
      {
-       test: /\.(js|jsx)$/,
+       test: /\.tsx?$/,
        exclude: /node_modules/,
        use: {
-         loader: "babel-loader"
+         loader: "ts-loader"
        }
  ...
};
```

entryや拡張子、そして先程インストールした`ts-loader`にloaderを変更します。

## 3. TypeScriptの設定(tsconfig.json)
次にTypeScriptのコンパイルに必要な設定ファイルとして`tsconfig.json`を追加します。

```bash
$ ./node_modules/.bin/tsc --init
```

`.tsconfig.json`でjsxの有効化やトランスパイルのターゲット、出力するモジュール方式などを設定します。
`noUnusedLocals`や`noUnusedParameters`など自由に設定してください。
`tsconfig.json`の各種プロパティはこちらの[TypeScript HandBook tsconfig.json](https://www.typescriptlang.org/docs/handbook/tsconfig-json.html)を参照してください。

今回は最小限以下のように設定しました。

```json
{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "noImplicitAny": true,
    "module": "ES2015",
    "target": "es5",
    "jsx": "react"
  }
}
```

## 4. 型定義ファイル`@types/`のインストール
TypeScriptではnpmパッケージとして型定義ファイルが提供されています。(TypeScript2.0系から)
Reactなどのnpmライブラリを使う場合、型情報として@types/xxxを一緒にインストールします。
またReduxのようにパッケージに型定義が同梱されている場合はインストールは不要です。

ReactでTypeScriptを利用する場合、Reactの型定義ファイル(`@types/react`と`@types/react-dom`)をインストールする必要があります。

```bash
$ npm i @types/{react,react-dom} --save-dev
```

## 5. 不要になったファイルやライブラリを削除
TypeScriptを導入したことで、不要になったファイルやライブラリを削除します。
ES5へのトランスパイルやjsx記法のコンパイルはTypeScriptで行うことができるので`Babel`が不要になります。
また、propsの型情報はTypeScriptで定義するので`prop-types`も不要になります。

```bash
$ npm remove @babel/core @babel/preset-env @babel/preset-react babel/loader
$ npm remove prop-types
$ rm .babelrc
```

## 6. Reactコンポーネントのファイルに型を書く(js -> tsx)
最後にReactコンポーネントに型を導入して、コンパイルを行います。
まずは拡張子を`.js`から`.tsx`にリネームしましょう。

```bash
$ git mv src/index.js src/index.tsx
$ git mv src/js/components/container/CounterContainer.js src/js/components/container/CounterContainer.tsx
$ git mv src/js/components/presentational/Button.js src/js/components/presentational/Button.tsx
```

あとはファイルの中身を書き換えていきます。

`index.tsx`の変更点は以下の通りです。

```diff
- import React, { Component } from "react";
- import ReactDOM from "react-dom";
+ import * as React from 'react';
+ import * as ReactDOM from 'react-dom';

import CounterContainer from "./js/components/container/CounterContainer";

const rootEl = document.getElementById('root')
rootEl ? ReactDOM.render(<CounterContainer />, rootEl) : false;
```

ここでは、`react`と`react-dom`のimportの書き方が変わります。

次に`CounterContainer.tsx`を修正します。
変更点は以下の通りです。

```diff
- import React, { Component } from "react";
+ import * as React from 'react';
  import Button from "../presentational/Button";

- class CounterContainer extends Component {
-   constructor() {
-     super();
-     this.state = {
-       count: 0
-     }
-     this.countUp = this.countUp.bind(this)
-   }
-   countUp() {

+ interface Props {}

+ interface State {
+   count: number;
+ }

+ export default class CounterContainer extends React.Component<Props, State> {
  
+ public state: State = {
+   count: 0
+ };

+ public countUp = () => {
    this.setState({count: this.state.count + 1})
  }

  render() {
    return (
      <div>
        <div>count:{this.state.count}</div>
        <Button label="count up!" onClick={this.countUp}/>
      </div>
    )
  }
}
- export default CounterContainer;
```

class Componentの定義は`React.Component<P, S>`をextendsして作成します。
`P`がpropsの型、`S`がstateの型となります。

最後に`Button.tsx`を修正します。
変更点は以下の通りです。

```diff
- import React from "react";
- import PropTypes from "prop-types";
+ import * as React from 'react';

+ interface Props {
+   label: string;
+   onClick: () => void;
+ }

- const Button = ({ label, onClick }) => (
+ const Button = (props: Props) => (
-   <button onClick={onClick}>{label}</button>
+   <button onClick={props.onClick}>{props.label}</button>
  );

- Button.propTypes = {
-   label: PropTypes.string.isRequired,
-   onClick: PropTypes.func.isRequired
- };
- 
  export default Button;
```

propTypesではなく、TypeScriptでpropsの型を定義します。

これで`tsx`ファイルの修正は終わりです。

## 7. tsxファイルをコンパイルする
最後に、これでコンパイルが通ることを確認します。

```bash
$ npm run build
> webpack --mode production

Hash: 18d8fcfcb0ded510ffa2
Version: webpack 4.26.1
Time: 1776ms
Built at: 2018-11-30 21:14:11
       Asset       Size  Chunks             Chunk Names
./index.html  281 bytes          [emitted]
   bundle.js    109 KiB       0  [emitted]  main
Entrypoint main = bundle.js
[7] ./src/index.tsx + 2 modules 1.88 KiB {0} [built]
    | ./src/index.tsx 279 bytes [built]
    |     + 2 hidden modules
    + 7 hidden modules
Child html-webpack-plugin for "index.html":
     1 asset
    Entrypoint undefined = ./index.html
    [0] ./node_modules/html-webpack-plugin/lib/loader.js!./src/index.html 265 bytes {0} [built]
```

無事、コンパイルすることができました！
コンパイル済みのファイルは`dist/bundle.js`に出力され、`dist/index.html`から読み込まれます。

最終的な構成は以下のようになりました。

```bash
$ tree
.
├── dist
│   ├── bundle.js
│   └── index.html
├── package.json
├── src
│   ├── index.html
│   ├── index.tsx
│   └── js
│       └── components
│           ├── container
│           │   └── CounterContainer.tsx
│           └── presentational
│               └── Button.tsx
├── tsconfig.json
└── webpack.config.js
```

# 参考記事
- [TypeScript handbook React & Webpack](https://www.typescriptlang.org/docs/handbook/react-&-webpack.html)
- [TypeScript handbook JSX](https://www.typescriptlang.org/docs/handbook/jsx.html)
- [React + TypeScript + Webpackの最小構成](https://qiita.com/uryyyyyyy/items/63969d6ed9341affdffb)
- [TypeScript and React using create-react-app: A step-by-step guide to setting up your first app](https://levelup.gitconnected.com/typescript-and-react-using-create-react-app-a-step-by-step-guide-to-setting-up-your-first-app-6deda70843a4)
- [サンプルプロジェクトのリポジトリ](https://github.com/toshi-toma/typescript-with-react-example)