Help us understand the problem. What is going on with this article?

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

はじめに

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

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

TypeScriptの公式サイト
TypeScriptのリポジトリ

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

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

Google Trendsを見ても、その人気が分かると思います。
typescript-trends.png
TypeScriptのトレンド動向

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

導入するReactのプロジェクト

今回、Reactで書かれたカウンターアプリをサンプルとして使います。
サンプルプロジェクトのリポジトリはこちらを参照ください。

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

  • index.jsを起点にしたReactのコンポーネント
  • トランスパイルやコンパイルはwebpack 4 + Babel 7
$ 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で簡単に行えます。

今回変更した内容はこのプルリクエストで確認できます。

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

1. TypeScriptのインストール

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

$ npm i typescript ts-loader --save-dev

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

2. webpackの設定(webpack.config.js)

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

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を追加します。

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

.tsconfig.jsonでjsxの有効化やトランスパイルのターゲット、出力するモジュール方式などを設定します。
noUnusedLocalsnoUnusedParametersなど自由に設定してください。
tsconfig.jsonの各種プロパティはこちらのTypeScript HandBook tsconfig.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)をインストールする必要があります。

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

5. 不要になったファイルやライブラリを削除

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

$ 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にリネームしましょう。

$ 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の変更点は以下の通りです。

- 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;

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

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

- 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を修正します。
変更点は以下の通りです。

- 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ファイルをコンパイルする

最後に、これでコンパイルが通ることを確認します。

$ 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から読み込まれます。

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

$ 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

参考記事

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away