maesblog

TypeScriptでReactを書く – webpackを使った開発環境の構築方法

2017年に入りTypeScriptがだいぶ熱くなってきています。特に以下の2つのニュースはTypeScriptの使用について大きく後押ししてくれるものとなったかと思います。

こうした動きを受けて、私もTypeScriptへの移行を模索し始めました。今回の記事では、私が現在最も書く機会の多いReactをTypeScriptで書く環境の構築方法を紹介したいと思います。

はじめに

TypeScriptは、言わずもがなMicrosoftが開発する静的型付けに対応したJavaScriptのスーパーセットです。

typescript

ReactをTypeScriptで書くメリットは、このTypeScriptの静的型チェックをReactの開発に導入できることです。特にReactでは、コンポーネント間でpropsを使った値の受け渡しが多発します。複数人で開発を行った際に、型の情報があると、他のメンバーの書いたコードの意図が明確になり、値の受け渡しの際のミスなどを防ぐことが可能となります

これまでReactでは、React.PropTypesで、propsの型チェック機能が付属していましたが、2017年4月7日にリリースされたReact v15.5.0においてこのReact.PropTypesはReact本体から切り離され、別パッケージとなりました。もちろん、別パッケージとなったprop-typesを使って、これまで通りpropsの型チェックを行うことも可能です。しかし、今回のReact.PropTypesの別パッケージ化は、FlowTypeScriptの使用を推奨する形で行われました。

TypeScriptでも、Reactを書くためのドキュメントが以下のように用意されています。今後はTypeScriptでReactを書く機会も増えていくのではないでしょうか。

ということで、TypeScriptでReactを書く環境の構築方法についてまとめてみました。参考にしていただき、ぜひこの機会にTypeScriptでReactを書く方法を模索してみてください。

なお、今回の説明では、以下のバージョンのものを使用しています。

npm環境の準備

プロジェクトディレクトリを作成し、その中でReact、TypeScript、webpackが使える環境を準備します。

まず、Reactの開発を進めていくプロジェクトディレクトリを作成します。今回はtypescript-react-projectとしましたが、好きなディレクトリ名をつけてかまいません。

$ mkdir typescript-react-project

プロジェクトディレクトリを作成したら、そこに移動して、npm initコマンドを叩きます。ディレクトリ内にpackage.jsonファイルが作成され、npmが使用できるようになります。

$ cd typescript-react-project
$ npm init

npmはNode.jsのパッケージ管理ツールです。npmを使用するには、Node.jsがインストールされている必要があります。Node.jsのインストールをまだしてない方は、当ブログの以下の記事などを参考にして事前にインストールしてください。

Reactの開発に必要なファイルの準備

次にプロジェクトで使うファイルを準備していきます。

今回の説明では、以下の3つのファイルを使用します。

  • Hello.tsx: ReactをTypeScriptで書くためのファイル
  • bundle.js: TypeScriptで書かれたHello.tsxがJavaScriptにトランスパイルされ、最終的にwebpackによってひとつにまとめられるファイル
  • index.html: bundle.jsを読み込んでReactコンポーネントが出力されるReactアプリケーション本体となるエントリーファイル

上記のファイルを以下のディレクトリ構成の元で作成します。(tsconfig.json、webpack.config.js、webpack-product.config.jsはTypeScriptとwebpackの設定ファイルです。詳細は後ほど説明します。)

typescript-react-project/
│
├─ src/
│   └─ Hello.tsx
│
├─ dist/
│   └─ bundle.js
│
├─ index.html
│
├─ tsconfig.json
├─ webpack.config.js
└─ webpack-product.config.js

作成したindex.htmlファイルを開き、以下のように記述します。webpackによって出力されたbundle.jsをこのindex.htmlファイルで読み込みます。

<!doctype html>

<html>

<head>
  <meta charset="utf-8">
  <title>Hello React & TypeSctipt</title>
</head>

<body>
  <div class="content"></div>
  <script src="./dist/bundle.js"></script>
</body>

</html>
index.html

これで最初の準備は整いました。以下より環境構築について説明していきます。

npmパッケージのインストール

今回の説明では、npmコマンドでのインストールとしていますが、yarnでインストールしても構いません。

webpackのインストール

webpackはプロジェクトに関わる全てのファイルを、それぞれの依存関係を解決しながら最終的にひとつのjsファイルにまとめてくれるツールです。

以下のコマンドでwebpackをプロジェクトディレクトリにインストールします。

$ npm install --save-dev webpack

reactとreact-domのインストール

reactは文字通りreactの本体で、react-domはreactで構築したDOM(仮想DOM)をブラウザで読める生のDOMに変換するためのパッケージです。

以下のコマンドでreactとreact-domをプロジェクトディレクトリにインストールします。

$ npm install --save react react-dom @types/react @types/react-dom

なお、ここでは、@types/という接頭辞をパッケージの前につけたものも一緒にインストールしています。これは、「types」という言葉からもわかるかと思いますが、reactとreact-domの型定義ファイルです。TypeScript 2から型定義ファイルをnpmで管理できるようになり、npm経由でのインストールが可能となりました。

これまでのようにtsdやTypingsで管理する必要がなくなり、複雑さが解消されているかと思います。型定義ファイルはnode_modules/@typesディレクトリ内に格納されているので、どのように型が定義されているのかを実際にファイルを見て確認することもできます。

typescript、awesome-typescript-loader、source-map-loaderのインストール

TypeScriptは文字通りTypeScript本体です。awesome-typescript-loaderは、設定ファイル(tsconfig.json)を元にTypeScriptで書かれたコードをwebpackでJavaScriptにトランスパイルするためのパッケージです。source-map-loaderは、TypeScriptの全エントリからソースマップを抽出し、webpackでソースマップを生成するためのパッケージです。

以下のコマンドでtypescriptとawesome-typescript-loaderとsource-map-loaderをプロジェクトディレクトリにインストールします。

$ npm install --save-dev typescript awesome-typescript-loader source-map-loader

なお、awesome-typescript-loaderの代わりにts-loaderを使っても良いです。ただawesome-typescript-loaderの方が速いとのことです。

TypeScriptの設定(tsconfig.json)

パッケージのインストールが完了したら、早速TypeScriptの設定を行います。tsconfig.jsonファイルを使って、TypeScriptのトランスパイル時の設定を行います。この設定を元にawesome-typescript-loaderとwebpackがTypeScriptで書かれたコードをJavaScriptにトランスパイルします。

まず、tsconfig.jsonファイルをプロジェクトディレクトリ直下に作成します。

$ touch tsconfig.json

tsconfig.jsonファイルを作成したら、ファイルを開いて以下のように記述します。

{
  "compilerOptions": {
    "outDir": "./dist/",
    "sourceMap": true,
    "strictNullChecks": true,
    "strictPropertyInitialization": true,
    "noImplicitAny": true,
    "noImplicitReturns": true,
    "module": "commonjs",
    "target": "es5",
    "jsx": "react"
  },
  "include": [
    "./src/**/*"
  ]
}
tsconfig.json

上記については、あくまでも一例です。tsconfig.jsonでの設定方法についての詳細は以下をご参照ください。

なお、今回のサンプルでは、strictNullChecks(nullとundefined値は全ての型の領域から外れ、自分自身とany以外の割り当てを禁止)、strictPropertyInitialization(undefinedを許容していないクラスのプロパティがコンストラクタで初期化されていないことを禁止)、noImplicitAny(暗黙なany型を使った式や宣言の禁止)、noImplicitReturns(関数内でコードパスが値を返さないことを禁止)にtrueを設定しています。これらは、トランスパイル時に厳格な型チェックを行うようにする設定です。まだTypeScriptに慣れていない場合は、状況に応じてfalseにしておくとよいです。

webpackの設定(webpack.config.js)

TypeScriptの設定が完了したら、次にwebpackの設定を行います。webpackの設定はwebpack.config.jsファイルで行います。その設定内容に基づいてTypeScriptのトランスパイルやReactのビルドが行われることになります。

まず、webpack.config.jsファイルをプロジェクトディレクトリ直下に作成します。

$ touch webpack.config.js

webpack.config.jsファイルを作成したら、ファイルを開いて以下のように記述します。

module.exports = {
  entry: "./src/Hello.tsx",
  output: {
    filename: "bundle.js",
    path: __dirname + "/dist"
  },

  // デバッグ用にソースマップの出力を有効にします。
  devtool: "source-map",

  resolve: {
    // 解決可能な拡張子として、'.ts' と '.tsx' を追加します。
    extensions: [".ts", ".tsx", ".js", ".json"]
  },

  module: {
    rules: [
      // 拡張子が .ts と .tsx のファイル を 'awesome-typescript-loader' で
      // 扱うようにします。
      { test: /\.tsx?$/, loader: "awesome-typescript-loader" },

      // 出力されるすべての .js ファイルは、'source-map-loader' で
      // 再加工されたソースマップを持ちます。
      { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
    ]
  },

};
webpack.config.js

webpack.config.jsファイルについての詳細は以下をご参照ください。

TypeScriptでReactを書く – Hello World

これでTypeScriptでReactを環境が整いました。それでは最後にTypeScriptでReactを書いてみましょう。TypeScriptによる静的型付けの感じを掴むためにフォームに入力した文字列を「Hello ○○」として出力するちょっとだけ複雑にした「Hello World」を実装していきます。

サンプル

以下サンプルとなります。実際に入力フォームに文字を入力し、「Say Hello」ボタンを押してみてください。

maechabin/typescript-react-demo

TypeScriptでのJSXについて

Reactで有名になったのがJSX構文です。JSXを使うと、Reactの出力するDOMをHTML(XML)のように書くことができます。JSXは最終的にJavaScriptに変換して使います。TypeScriptでもJSX(TSX)をサポートしていて、埋め込み、型チェック、JavaScriptへのトランスパイルなどができるようになっています

TypeScriptでJSXを使うには以下の2点について行う必要があります。

  • ファイルの拡張子を.tsxにする
  • tsconfig.jsonでjsxオプションを有効にする

詳細については以下をご参照ください。

TypeScriptで書いたReactのコード

上記のHello WorldのサンプルをTypeScriptで書くと以下のようになります。

/* reactとreact-domの読み込み */
import * as React from 'react';
import * as ReactDOM from 'react-dom';

/** Helloコンポーネントで取得するpropsの型定義 */
interface HelloProps {
  greeting: string;
}
/** Helloコンポーネントのstateの型定義 */
interface HelloState {
  inputName: string;
  outputName: string;
}
/** Helloコンポーネント */
class Hello extends React.Component<HelloProps, HelloState> {
  constructor(props: HelloProps) {
    super(props);
    this.state = {
      inputName: '',
      outputName: '',
    };
    this.handleChange = this.handleChange.bind(this);
    this.handleClick = this.handleClick.bind(this);
  }
  handleChange(event: React.FormEvent<HTMLInputElement>): void {
    this.setState({
      inputName: event.currentTarget.value,
    });
  }
  handleClick(): void {
    this.setState({
      inputName: '',
      outputName: this.state.inputName,
    });
  }
  render(): JSX.Element {
    const { greeting } = this.props;
    return (
      <div>
        <Input
          name={this.state.inputName}
          handleChange={this.handleChange}
        />
        <Button handleClick={this.handleClick} />
        <Output greeting={greeting} name={this.state.outputName} />
      </div>
    );
  }
}

/** Inputコンポーネントで取得するpropsの型定義 */
interface InputProps {
  name: string;
  handleChange: (event: React.FormEvent<HTMLInputElement>) => void;
}
/** Inputコンポーネント */
const Input: React.SFC<InputProps> = props => {
  const { name, handleChange }: InputProps = props;
  return (
    <input
      type="text"
      placeholder="Input any name."
      value={name}
      onChange={handleChange}
    />
  );
};

/** Buttonコンポーネントで取得するpropsの型定義 */
interface ButtonProps {
  handleClick: () => void;
}
/** Buttonコンポーネント */
const Button: React.SFC<ButtonProps> = props => {
  const { handleClick }: ButtonProps = props;
  return <button onClick={handleClick}>Say Hello</button>;
};

/** Outputコンポーネントで取得するpropsの型定義 */
interface OutputProps {
  greeting: string;
  name: string;
}
/** Outputコンポーネント */
const Output: React.SFC<OutputProps> = props => {
  const { greeting, name }: OutputProps = props;
  const hasName: boolean = name !== '';
  const result: JSX.Element | '' = hasName ? (
    <h1>
      {greeting} {name}!
    </h1>
  ) : (
    ''
  );
  return <div>{result}</div>;
};

/** Bootstraping */
ReactDOM.render(
  <Hello greeting="Hello!!" />,
  document.querySelector('.content'),
);
Hello.tsx

Reactの型定義については、「node_modules > @type > react > index.d.ts」ファイルを見てもらえるとよいかと思います。ちなみに、代表的なものに関しては、以下のように定義されています。

  • Component: class Component<P, S> implements ComponentLifecycle<P, S> {…}
  • PureComponent: class PureComponent<P, S> extends Component<P, S> { }
  • Functional Component: type SFC<P> = StatelessComponent<P>;
  • JSX: JSX.Element

また、TypeScriptの構文の説明については公式ドキュメントなどをご参照ください。

このサンプルのコードは、GitHubにもアップしていますので、併せてご確認ください。

webpackの実行(TypeScriptのトランスパイル)

ReactのコードをTypeScriptで書いたら、最後に有効なJavaScriptに変換してブラウザで読めるようにします。すでにTypeScriptとwebpackの設定は上記で完了しているので、後はwebpackを実行すれば良いだけとなっています。

webpackを実行するにはnpmコマンドを使用します。package.jsonファイルにwebpackを実行するコマンドを登録することで、「npm run ○○」としてコマンドを実行することができるようになります。

package.jsonファイルを開いて、scriptフィールドの部分に以下を記述します。

{
  ...
  "scripts": {
    "build": "webpack"
  },
  ...
}
package.json

package.jsonファイルにコマンドを登録したら、以下のコマンドでwebpackが実行され、ビルド結果がbundle.jsファイルに出力されます。

$ npm run build

なお、このタイミングでTypeScriptの静的型チェックが行われます(型に問題があればトランスパイルエラーとなります)。また同時に型アノテーションとインターフェイスがコードから取り除かれます

問題なくファイルがビルドされたら、index.htmlをブラウザで開いて内容を確認してみましょう。

ファイルの更新を監視してwebpackを実行

上記のコマンドだと、ファイルを更新するたびにnpm run buildを叩く必要があり、正直面倒です。watchオプションをつけてwebpackを実行すると、ファイルの更新を監視するようになります。

以下をpackage.jsonファイルに追記します。

{
  ...
  "scripts": {
    "build": "webpack",
    "watch": "webpack --watch"
  },
  ...
}
package.json

以下のコマンドを叩くと、ファイルの更新を監視するようになり、ファイルを保存するたびにTypeScriptによる型のチェックと、ビルドが行われるようになります。

$ npm run watch

開発時はbundle.jsにreactとreact-domを含めないようにする

上記のように、ファイルの更新を監視して、ファイルを保存するたびにwebpackを実行することになると、毎回reactreact-dombundle.jsファイルに含めようとすると、ビルドの時間がかかります。

そこでwebpack.config.jsファイルのexternalsフィールドを追加して、以下のように記述すると、webpackはグローバル変数のReactReactDOMを通してreact、react-domを扱うようになります。

module.exports = {
  ...
  externals: {
    "react": "React",
    "react-dom": "ReactDOM"
  },
};
webpack.config.js

従って、この場合index.htmlファイルの方に、script要素を使ってreact.jsファイルとreact-dom.jsファイルを読み込むようにします。

<script src="./node_modules/react/dist/react.js"></script>
<script src="./node_modules/react-dom/dist/react-dom.js"></script>
webpack.config.js

こうすることで、ブラウザのキャッシュが効くようになり、開発もさくさく進むようになるかと思います。お試しください。

製品版としてビルドする

最後に、製品版としてビルドする方法を紹介します。Reactはデフォルトでは、ディペロップメント(開発者)モードになっていて、余計な情報が含まれています。開発時には必要なこれらの情報も、製品版には必要ありません。そこで、製品版としてビルドする際は、これらの情報を含まないようにしてあげる必要があります。そうすることで、アプリケーション自体の高速化も実現できます。

設定ファイルの準備

webpackの設定ファイルとしてwebpack.config.jsファイルを作成しましたが、それとは別に製品版のビルド用として設定ファイルを新たに作ります。通常の設定ファイルと区別するためにwebpack-production.config.jsといった任意のファイル名をつけます。

$ touch webpack-production.config.js

webpack-production.config.jsファイルを作成したら、ファイルを開いて以下のように記述します。webpack.config.jsファイルに追記する形となります。追記した部分にはマーカーをつけておきます。

/* webpackを読み込みます */
var webpack = require('webpack');

module.exports = {
  entry: "./src/Hello.tsx",
  output: {
    filename: "bundle.js",
    path: __dirname + "/dist"
  },

  // デバッグ用にソースマップの出力を有効にします。
  devtool: "source-map",

  resolve: {
    // 解決可能な拡張子として、'.ts' と '.tsx' を追加します。
    extensions: [".ts", ".tsx", ".js", ".json"]
  },

  module: {
    rules: [
      // 拡張子が .ts と .tsx のファイル を 'awesome-typescript-loader' で
      // 扱うようにします。
      { test: /\.tsx?$/, loader: "awesome-typescript-loader" },

      // 出力されるすべての .js ファイルは、'source-map-loader' で
      // 再加工されたソースマップを持ちます。
      { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
    ]
  },
  /** プラグインの設定 */
  plugins: [
    /** DefinePluginの実行 */
    new webpack.DefinePlugin({
      // process.env.NODE_ENVを'production'に置き換える
      'process.env.NODE_ENV': JSON.stringify('production')
    }),
    /** UglifyJsPluginの実行 */
    new webpack.optimize.UglifyJsPlugin({
      compress: {
        // 圧縮する時に警告を除去する
        warnings: false
      }
    })
  ],
};
webpack-production.config.js

製品版のビルド用コマンドを登録

webpack-production.config.jsの準備が完了したら、製品版のビルド用コマンドを登録をpackage.jsonファイルに登録します。package.jsonファイルを開いて、scriptフィールドの部分に以下を記述します。

{
  ...
  "scripts": {
    "watch": "webpack --watch",
    "build": "webpack",
    "build:production": "webpack --config webpack-production.config.js"
  },
  ...
}
webpack-production.config.js

webpackの実行し、製品版としてビルド

以下のコマンドを実行すると、製品版としてbundle.jsファイルにビルドされます。

$ npm run build

create-react-appを使う

長々と開発環境の構築方法を説明してきましたが、create-react-appを使ってTypeScriptでReactを書く環境を構築することもできます。最後に簡単に触れておきます。

create-react-appはFacebookが開発するコマンド1つでReactの開発環境を構築してくれる便利なツールです。create-react-appについての詳細は以下をご参照ください。

このcreate-react-appでプロジェクトディレクトリを作る際に以下のようにscripts-version=react-scripts-tsというオプションをつけてコマンドを叩くだけで、TypeScriptに対応したReactの開発環境が構築されます。

$ create-react-app typescript-react-project --scripts-version=react-scripts-ts

使い方などの詳細は以下をご参照ください。

まとめ

以上で、TypeScriptでReactを書くための環境が整いました。普段Babelで開発環境を整えている方はわかると思いますが、Babelの部分がTypeScriptに置き換わっただけですね。それから今回は触れませんでしたが、TSLintによる構文チェック、webpackのHMR(Hot Module Replacement)なども準備しておくと、より開発が快適になります。この辺りは必要に応じて各自準備などしてもらえればと思います。

今回の記事では、以下について説明しました。もう一度見直したい部分がありましたら、見直してみてください。

  • 書くパッケージのインストール
  • tsconfig.jsonによるのTypeScriptの設定
  • webpack.config.jsによるwebpackの設定
  • 開発時のビルドについて
  • 製品版のビルドについて

今回は特にTypeScriptの書き方については、詳しくは説明しませんでした。自分もまだまだあやふやな点が多いです。ポイントとしては、インターフェイスジェネリックスの使い方が肝になってきますね。

TypeScriptをこれから勉強するに当たって、基本的には公式ドキュメントを読むのがベストでしょう。書籍では以下のものが今年の2月に発売されているので、役に立ちそうかなと思っています。最後に紹介しておきます。

はじめてのTypeScript2
  • 『はじめてのTypeScript2 』
  • 著者: 清水美樹
  • 出版社: 工学社
  • 発売日: 2017年2月

関連記事

コメント

  • 必須

コメント