環境構築
TypeScript
ライブラリ
React

2019年版: 脱Babel!フロント/JS開発をTypeScriptに移行するための環境整備マニュアル

TL;DR

いろいろ書いていますが、一番書きたかったのは最初のライブラリと最後のReact Componentのプロジェクトの作り方ですね。ぱっとnpm installして、最初から型定義ファイルが入っていて、@typesを持っているライブラリを探したり、自分で.d.tsを書いたりしなくてもいい世界がやってきて欲しいな、という気持ちから書いています。

ここで紹介したTypeScript環境構築はすべて、自分用にYeomanのテンプレートとして作成したので、以下のジェネレータをインストールして選択したらそれでおしまいです。

  • @shibukawa/typescript (npmには公開していないので、checkoutしてビルドしてインストールしてください)

まえがき

フューチャーアドベントカレンダーの先頭打者です。

1年近くお仕事でJavaScriptのES2017とReact(というかNext.js)を使った開発をしてきて、開発当初の環境たち上げや開発そのものは快適だったのですが、Next.js、ava、Storybookという、「Babel/webpackを内包して簡単に使い始められるよ」系のツールやライブラリのバージョンアップで、Babelのバージョンがあわなくて、なかなか最新バージョンにあげるのが困難ということが起きました。ライブラリのバージョンアップのお守りに工数取られたくはないですよね。

というわけで、Babel依存を減らし、npmで配布するライブラリ、CLIコマンド、Reactのウェブフロントエンド、npm で配布するReactのコンポーネントの4つの種類のプロジェクトをTypeScriptで完璧に立ち上げるための環境設定を考えます。特に、ウェブ開発はともかく、ライブラリやコンポーネントのための環境構築の視点の記事は今のところ見かけたことがないので。なお、JSツールとかライブラリが増えると組み合わせが面倒になるので、依存ツールとかライブラリは極力増やさないようにしています。

なお、それぞれのツールのカテゴリーなどについては説明していませんし、ツール選定もそれぞれのカテゴリーの中で1つに決めてしまっています。もし、わからない用語について確認したい、他の選択肢についての解説が欲しいなどがあれば、Modern JavaScript概観、そしてElectronへというエントリーが参考になります。

基本的なツール選定

基本的にすべてのプロジェクトでJest、TSLint、Prettierなどを選択しています。まあ、どれも相性問題が出にくい、数年前から安定して存在しているといった保守的な理由ですね。きちんと選べば、「JSはいつも変わっている」とは距離を置くことができます。

  • Jest

    テスティングフレームワークはたくさんありますが、avaとJestがテスト並列実行などで抜きん出ています。JestはTypeScript用のアダプタが完備されています。avaはBabel/webpackに強く依存しており、単体で使うなら快適ですが、他のBabel Configと相性が厳しくなるのでJestにしています。

  • TSLint

    まあ、TypeScriptだと選択肢は他にはないですよね?

  • Prettier

    typescript-formatterもありますが、TSLintの--fixと連携して動くための設定が簡単だったり、TypeScript以外のSCSSとかにも対応していたりするのでこれで。gtsは裏でclang-formatを動かすらしい・・・試してないけど、Windowsとかでも簡単に入るんですかねそれ・・・

  • npm scripts

    ビルドは基本的にMakefileとかgulpとかgruntとかを使わず、npm scriptsで完結するようにします。ただし、複数タスクをうまく並列・直列に実行する、ファイルコピーなど、Windowsと他の環境で両対応のnpm scriptsを書くのは大変なので、mysticateaさんのQiitaのエントリーのnpm-scripts で使える便利モジュールたちを参考に、いくつかツールを利用します。

  • Visual Studio Code

    TypeScript対応の環境で、最小設定ですぐに使い始められるのはVisual Studio Codeです。しかも、必要な拡張機能をプロジェクトファイルで指定して、チーム内で統一した環境を用意しやすいので、推奨環境として最適です。もちろん、フォーマッターなどはコマンドラインでも使えるようにはするので、腕に覚えのある人はVimでもEmacsでもなんでも使ってくれ、という感じです。

ライブラリ開発の環境設定

最初なのでちょっと説明が長めです。

現在、JavaScript系のものは、コマンドラインのNode.js用であっても、Web用であっても、なんでも一切合切npmjsにライブラリとしてアップロードされます。ここでのゴールは可用性の高いライブラリの実現で、具体的には次の4つの目標を達成します。

  • 特別な環境を用意しないと(ES6 modules構文使っていて、素のNode.jsでnpm installしただけで)エラーになったりするのは困る
  • npm installしたら、型定義ファイルも一緒に入って欲しい
  • 今流行りのTree Shakingに対応しないなんてありえないよね?
  • もちろん、開発側の手間は減らす(依存ツールとかは最小)

今の時代でも、ライブラリはES5形式で出力はまだまだ必要です。uglify.jsがES6でも腹を下さないようになったので、開発で変にトラブルが発生することはだいぶ減ったとはいえ、まだまだインターネットの世界で利用するには古いブラウザ対応が必要だったりもします。

なお、ライブラリの場合は特別な場合を除いてBabelもwebpackもいりません。CommonJS形式で出しておけば、Node.jsで実行するだけなら問題ありませんし、基本的にwebpackとかrollup.jsとかのバンドラーを使って1ファイルにまとめるのは、最終的なアプリケーション作成者の責務となります。特別な場合というのは、Babelプラグインで加工が必要なJSS in JSとか、flowtypeとかですが、ここでは一旦おいておきます。ただし、JSXはTypeScriptの処理系自身で処理できるので不要です。

作業フォルダを作る

$ mkdir awesome-lib
$ cd awesome-lib
$ npm init -y
$ mkdir src
$ mkdir __tests__
$ mkdir dist-cjs
$ mkdir dist-esm

zshならmkdirで複数ディレクトリを列挙したら1行でできますが、Windowsはできないっぽいので({}と,でできるらしいけど)、1行ごとに変えています。

出力フォルダをCommonJSと、ES6 modules用に2つ作っています。webpackとかrollup.jsのTree ShakingではES6 modules形式のライブラリを想定しています。また、Node.jsは--experimental-modulesを使わないとES6 modulesはまだ使えませんし、TypeScriptが拡張子を.mjsにできないし、Browserifyを使いたいユーザーもいると思うので、CommonJS形式も必須です。

srcフォルダ以下に.tsファイルを入れて、distフォルダ以下にビルド済みファイルが入るイメージです。distはgitでは管理しませんので.gitignoreに入れておきます。

.gitignore
dist-cjs/
dist-esm/

それとは逆に、配布対象はdistとルートのREADMEとかだけですので、不要なファイルは配布物に入らないように除外しておきましょう。これから作るTypeScriptの設定ファイル類も外して起きましょう。

.npmignore
__tests__/
src/
tsconfig.json
tslint.json
jest.config.json
.travis.yml
.editorconfig
.vscode

ビルドのツールのインストールと設定

まず、最低限、インデントとかの統一はしたいので、editorconfigの設定をします。editorconfigを使えばVisual Studio、vimなど複数の環境があってもコードの最低限のスタイルが統一されます(ただし、各環境で拡張機能は必要)。また、これから設定するprettierもこのファイルを読んでくれます。

.editorconfig
root = true

[*]
indent_style = space
indent_size = 4
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

ツール群はこんな感じで入れました。

$ npm install --save-dev typescript prettier
    tslint tslint-config-prettier tslint-plugin-prettier npm-run-all

開発したいパッケージがNode.js環境に依存したものであれば、Node.jsの型定義ファイルも入れておきます。

$ npm install --save-dev @types/node

設定ファイルは以下のコマンドを起動すると雛形を作ってくれます。これを編集して行きます。

$ tsc --init

とりあえずこんな感じにしてみました。大事、というのは今回の要件の使う側が簡単なように、というのを達成するための.d.tsファイル生成と、出力形式のES5のところと、入力ファイルですね。source-mapもついでに設定しました。あとはお好みで。

tsconfig.json
{
  "compilerOptions": {
    "target": "es5",           // 大事
    "declaration": true,       // 大事
    "declarationMap": true,
    "sourceMap": true,         // 大事
    "lib": ["dom", "ES2017"],
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true
  },
  "include": ["src/**/*"]      // 大事
}

TSLintの設定も作ります。Prettierと連携するようにします。console.logぐらいは許可して欲しい。

tslint.json
{
  "rulesDirectory": [
    "tslint-plugin-prettier"
  ],
  "extends": [
    "tslint:recommended",
    "tslint-config-prettier"
  ],
  "rules": {
    "no-console": [
        false
    ],
    "prettier": [
      true
    ]
  }
}

package.jsonで手を加えるべきは次のところぐらいですね。まず、つくったライブラリを読み込むときのエントリーポイントをmainで設定します。src/index.tsというコードがあって、それがエントリーポイントになるというのを想定しています。

scriptsは、buildでコンパイル、lintで文法チェック、fixでフォーマッターで修正、という最低限です。ライブラリの場合は次のようになりました。

package.json(ライブラリ用)
  "main": "dist-cjs/index.js",
  "module": "dist-esm/index.js",
  "types": "dist-cjs/index.d.ts",
  "scripts": {
    "build": "npm-run-all -s build:cjs buid:esm",
    "build:cjs": "tsc --project . --module commonjs --outDir ./dist-cjs",
    "build:esm": "tsc --project . --module es2015 --outDir ./dist-esm",
    "lint": "tslint --project .",
    "fix": "tslint --fix --project ."
  }

テスト

ユニットテスト環境も作ります。TypeScriptを事前に全部ビルドしてからJasmineとかも見かけますが、公式でTypeScriptを説明しているjestにしてみます。

$ npm install --save-dev jest ts-jest @types/jest

scripts/testと、jestの設定を追加します。古い資料だと、transformの値がnode_modules/ts-jestなんちゃらになっているのがありますが、今はts-jestだけでいけます。

package.json
  "scripts": {
    "test": "jest"
  }
jest.config.js
module.exports = {
    transform: {
        "^.+\\.tsx?$": "ts-jest"
    },
    moduleFileExtensions: [
        "ts",
        "tsx",
        "js",
        "json",
        "jsx"
    ]
};

Visual Studio Codeの設定

Visual Stuido Codeでフォルダを開いたときに、tslintの拡張と、editorconfigの拡張がインストールされるようにします。

.vscode/extensions.json
{
    "recommendations": [
        "eg2.tslint",
        "EditorConfig.editorconfig"
    ]
}

ファイル保存時にtslint --fixが自動実行されるように設定しておきましょう。これでVisual Studio Codeを使う限り、誰がプロジェクトを開いてもコードスタイルが保たれます。

.vscode/settings.json
{
    "tslint.autoFixOnSave": true
}

ライブラリコード

これまでの設定ファイルはsrc/index.tsというファイルがエントリポイントであると想定してきました。もちろん、package.jsonのmain/module/typesの項目を修正すれば、どのファイルでもエントリーポイントにできます。main.tsが良い人もいるでしょう。

index.tsがエントリーポイントなので、ここでexportしたものがライブラリが提供するAPIとなります。exportはES6形式で書きます。

index.ts
export function hello() {
    console.log("Hello from TypeScript Library");
}

まとめと、普段の開発

アルゴリズムなどのロジックのライブラリの場合、webpackなどのバンドラーを使わずに、TypeScriptだけを使えば良いことがわかりました。ここにある設定で、次のようなことが達成できました。

  • TypeScriptでライブラリのコードを記述する
  • 使う人は普段通りrequire/importすれば、特別なツールやライブラリの設定をしなくても適切なファイルがロードされる。
  • 使う人は、別途型定義ファイルを自作したり、別パッケージをインストールしなくても、普段通りrequire/importするだけでTypeScriptの処理系やVisual Stuido Codeが型情報を認識する
  • Tree Shakingの恩恵も受けられる

package.jsonのscriptsのところがすべてですが、いろいろな開発にともなうコマンドはnpmコマンドを使って行えます。

# ビルドしてパッケージを作成
$ npm run build
$ npm pack

# テスト実行 (VSCodeだと、⌘ R Tでいける)
$ npm test

# 文法チェック
$ npm run lint

# フォーマッター実行
$ npm run fix

CLIツール

さて、ライブラリの説明は最初ということもあって長文になってしまいましたが、今後は差分の手順だけを説明していくので短くなります。逆に、興味がなくても、ライブラリのところには目を通している前提で説明していきます。

作業フォルダを作る

ライブラリの時は、ES6 modulesとCommonJSの2通り準備しましたが、CLIの場合はNode.jsだけ動かせば良いので、出力先もCommonJSだけで十分です。

$ mkdir awesome-cmd
$ cd awesome-cmd
$ npm init -y
$ mkdir src
$ mkdir __tests__
$ mkdir dist   # commonJSだけなので1つだけ

.npmignoreはライブラリの説明と同じですが、.gitignoreの方は、出力先フォルダを1つだけに修正しましょう。

.gitignore
dist/

ビルドのツールのインストールと設定

基本的部分はライブラリの説明とまったく同じです。ついでに、コマンドラインで良く使うであろうライブラリを追加しておきます。カラーでのコンソール出力、コマンドライン引数のパーサ、ヘルプメッセージ表示です。どれもTypeScriptの型定義があるので、これも落としておきます。

$ npm install --save cli-color command-line-args command-line-usage
$ npm install --save-dev @types/cli-color @types/command-line-args @types/command-line-usage @types/node
$ npm install --save-dev jest @types/jest
$ npm install --save-dev typescript prettier
    tslint tslint-config-prettier tslint-plugin-prettier npm-run-all

出力先が1つになったので、引数違いで使い分けていた項目はtsconfig.jsonの中に取り込むことができます。

tsconfig.json
{
  "compilerOptions": {
    "target": "es2017",        // お好みで変更
    "declaration": false,      // 生成したものを他から使うことはないのでfalseに
    "declarationMap": false,   // 同上
    "sourceMap": false,        // 同上
    "lib": ["dom", "ES2017"],
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "module": "commonjs",      // 変更
    "outDir": "./dist"         // 変更
  },
  "include": ["src/**/*"]
}

package.jsonもビルドは1種類だけなので、ビルドの項目は減らせます。ただし、main/modules/typesの項目は不要です。import/requireして使うことはないはずです。変わりに、bin項目でエントリーポイント(ビルド結果の方)のファイルを指定します。

package.json(CLIツール用)
  "bin": {
    "awesome-cmd": "dist/index.js"
  },
  "scripts": {
    "build": "tsc --project .",
    "lint": "tslint --project .",
    "fix": "tslint --fix --project ."
  }

テストの設定、VSCodeの設定は変わりません。

CLIツールのソースコード

TypeScriptはシェバング(#!)があると特別扱いしてくれます。必ず入れておきましょう。ここで紹介したcommand-line-argsとcommand-line-usageはWikiで用例などが定義されているので、実装イメージに近いものをベースに加工していけば良いでしょう。

index.ts
#!/usr/bin/env node

import * as clc from "cli-color";
import * as commandLineArgs from "command-line-args";
import * as commandLineUsage from "command-line-usage";

async function main() {
    // 内部実装
}

main();

まとめと、普段の開発

コマンドラインツールの場合もライブラリ同様、webpackなどのバンドラーを使わずに、TypeScriptだけを使えば大丈夫です。ここにある設定で、次のようなことが達成できました。

  • TypeScriptでCLIツールのコードを記述する
  • 使う人は普段通りnpm installすれば実行形式がインストールされ、特別なツールやライブラリの設定をしなくても利用できる。

なお、npm scriptsはライブラリの時と同様です。

CLIツール(バンドル版)

npmで配るだけであればバンドルは不要ですが、ちょっとしたスクリプトをDockerサーバーで実行したいが、コンテナを小さくしたいのでnode_modulesは入れたくないということは今後増えていくと思いますので、そちらの方法も紹介します。

バンドラーでは一番使われているのは間違いなくwebpackですが、昔ながらのBrowserifyは設定ファイルレスで、ちょっとした小物のビルドには便利です。BrowserifyはNode.jsのCommonJS形式のコードをバンドルしつつ、Node.js固有のパッケージは互換ライブラリを代わりに利用するようにして、Node.jsのコードをそのままブラウザで使えるようにするものです。オプションでNode.jsの互換ライブラリを使わないようにもできます。

webpackでもちょっと設定ファイルを書けばいけるはずですが、TypeScript対応以外に、shebang対応とかは別のプラグインが必要だったり、ちょっと手間はかかります。

作業フォルダを作る

ここは、前述のCLIツール簡易版と同じです。

ビルドツールのインストールと設定

CLIツール簡易版の設定をまずはそのまま行い、ついでにbrowserifyとtsifyを入れます。tsifyはBrowserifyプラグインで、TypeScriptのコードを変換します。

$ npm install --save-dev browserify tsify

tsconfig.jsonに関してはそのままで問題ありません。この手のバンドラーから使われる時は、.tsと.jsの1:1変換ではなくて、複数の.tsを.jsにしたあとにまとめて1ファイルにするn:1変換になるため、noEmit: trueをつけたりdistDirを消したりする必要がありますが、そのあたりはtsifyが勝手にやってくれます。

ビルドスクリプトは次の形式になります。-pでtsifyを追加しています。もしminifyとかしたくなったら、minifyifyなどの別のプラグインを利用します。

package.json
"scripts": {
    "build": "browserify --node -o dist/script.js -p [ tsify -p . ] src/index.ts"
}

これで、TypeScript製かつ、必要なライブラリが全部バンドルされたシングルファイルなスクリプトができあがります。

CLIツールのソースコード

これも、簡易版のCLIと変更は変更ありません。

まとめと、普段の開発

これも、簡易版のCLIと変更は変更ありません。

なお、ネイティブビルドが必要なライブラリがあると、難易度がぐっと上がります。まず、Browserifyのexcludeで該当パッケージをバンドルしないように除外します。その後、node_modulesの該当パッケージは別にコピーしてあげる必要があるでしょう。Dockerで使う場合には、最小のNode.jsのDockerイメージを目指すスレの最後の部分を参考にしてください。ただ、そのネイティブコードから利用される別の非ネイティブなパッケージもぜんぶ用意はしておかないといけないはずで、労力の分はあんまり報われないと思うので、その場合はバンドルを諦めた方が良いかもしれません。

将来的に、次のツールがTypeScriptに対応したら(予定はされているらしい)、もうちょっと簡単になるかもしれません。

Reactアプリケーション開発

設定が辛くなってくるのがウェブのフロントエンドです。なんだかんだでシェアが一番高いというReactを取り上げます。

なお、Vue.jsは3.0でTypeScriptでリライトされて使い勝手がよくなるらしいので、とりあえずそれまでは触らないで置こうかと思っています。また、Angularは何もしなくても最初からTypeScriptです。最高ですね。

なるべく、いろんなツールとの組み合わせの検証の手間を減らすために、Next.jsをつかいます。JavaScriptは組み合わせが多くて流行がすぐに移り変わっていつも環境構築させられる(ように見える)とよく言われますが、組み合わせが増えても検証されてないものを一緒に使うのはなかなか骨の折れる作業で、結局中のコードまで読まないといけなかったりとか、環境構築の難易度ばかりが上がってしまいます。特にRouterとかすべてにおいて標準が定まっていないReactはそれが顕著です。

その中において、CSS in JS、RouterをオールインワンにしてくれているNext.jsは大変助かります。issueのところでもアクティブな中の人がガンガン回答してくれていますし、何よりも多種多様なライブラリとの組み合わせをexampleとして公開してくれているのが一番強い。Server Side Renderingもありますが、お仕事でやっていて一番ありがたいのはこの設定周りです。

作業フォルダを作る

Next.jsではpagesフォルダにおいてあるコンポーネントがRouterに自動登録されるので、このフォルダをとりあえず作ります。あとは、それ以外のコード置き場としてsrc、テストとして__tests__フォルダをそれぞれ作ります。

$ mkdir awesome-web
$ cd awesome-web
$ npm init -y
$ mkdir src
$ mkdir __tests__

ウェブサービスをnpmに公開することはあんまりないと思うので、.npmignoreは不要ですが、.gitignoreの方は、Next.jsのファイル生成先の出力先フォルダを設定しておきます。

.gitignore
.next

ビルドのツールのインストールと設定

Next.jsではnext以外にも、react、react-domをインストールします。他にも必要なものを入れてしまいましょう。ReactのJSXに対応させるために、tslint-reactを忘れないようにしましょう。

$ npm install --save next react react-dom @types/node
     @types/next @types/react @types/react-dom
$ npm install --save-dev typescript prettier npm-run-all
    tslint tslint-config-prettier tslint-plugin-prettier tslint-react 
$ npm install --save-dev jest ts-jest @types/jest

Next.jsを快適にするために、TypeScriptと、SCSSを入れます。Next.jsでは、本家が提供しているプラグインを使います。

$ npm install --save-dev @zeit/next-typescript @zeit/next-sass node-sass

Next.jsだけでは真っ白なシンプルなHTMLになってしまうので、よくメンテナンスされているMaterial Designのライブラリである、Material UIを入れましょう。ウェブ開発になると急に必要なパッケージが増えますね。

$ npm install --save @material-ui/core @material-ui/icons

tsconfig.jsonは今までと少し異なります。後段でBabelが処理してくれる、ということもあって、モジュールタイプはES6 modules形式、ファイルを生成することはせず、Babelに投げるのでnoEmit: true。ReactもJSX構文をそのまま残す必要があるので"preserve"。また、JSで書かれたコードも一部あるので、allowJsも: trueでなければなりません。

tsconfig.json
{
  "compilerOptions": {
    "allowJs": true,
    "allowSyntheticDefaultImports": true,
    "baseUrl": ".",
    "jsx": "preserve",
    "lib": ["dom", "es2017"],
    "module": "esnext",
    "moduleResolution": "node",
    "noEmit": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext"
  }
}

TypeScriptと、SCSSのプラグインを有効化します。

next.config.js
const withTypescript = require('@zeit/next-typescript');
const withSass = require('@zeit/next-sass');

module.exports = withTypescript(withSass({
    webpack(config, options) {
      return config
    }
}));

Next.jsの場合は、nextコマンドがいろいろやってくれるので、やっていることの分量のわりにscriptsがシンプルになります。いいですね。

package.json
  "scripts": {
    "dev": "next",
    "build": "next build",
    "export": "next export",
    "start": "next start",
    "lint": "tslint --project .",
    "fix": "tslint --fix --project .",
    "test": "jest",
    "watch": "jest --watchAll"
  }

Next.js+TSのソースコード

まずMaterial UIを使うときに設定しなければならないコードがあるので、Material UIのサンプルページのsrc/getPageContext.jspages/_app.jspages/_document.jsの3つのファイルをダウンロードして同じように起きます。Material UIのCSS in JSがNext.js標準の方法と違うので、それを有効化してやらないと、サーバーサイドレンダリングのときに表示がおかしくなってしまいます。

次にページのコンテンツです。Next.jsの規約としては、pages以下のファイルが、export defaultでReactコンポーネントを返すと、それがページとなります。ちょっと長いですが、TypeScriptでページ作成するための方法を色々埋め込んであります。

pages/index.tsx
import Link from "next/link";
import React from "react";

import { Toolbar } from "@material-ui/core";
import AppBar from "@material-ui/core/AppBar";
import Button from "@material-ui/core/Button";
import Dialog from "@material-ui/core/Dialog";
import DialogActions from "@material-ui/core/DialogActions";
import DialogContent from "@material-ui/core/DialogContent";
import DialogContentText from "@material-ui/core/DialogContentText";
import DialogTitle from "@material-ui/core/DialogTitle";
import {
    createStyles,
    Theme,
    withStyles,
    WithStyles
} from "@material-ui/core/styles";
import Typography from "@material-ui/core/Typography";

function styles(theme: Theme) {
    return createStyles({
        root: {
            paddingTop: theme.spacing.unit * 20
        }
    });
}

interface IProps {
    children?: React.ReactNode;
}

interface IState {
    openDialog: boolean;
}

class Index extends React.Component<
    IProps & WithStyles<typeof styles>,
    IState
> {
    public state = {
        openDialog: false
    };

    constructor(props: IProps & WithStyles<typeof styles>) {
        super(props);
    }

    public handleCloseDialog = () => {
        this.setState({
            openDialog: false
        });
    };

    public handleClickShowDialog = () => {
        this.setState({
            openDialog: true
        });
    };

    public render() {
        const { classes } = this.props;
        const { openDialog } = this.state;

        return (
            <div className={classes.root}>
                <Dialog open={openDialog} onClose={this.handleCloseDialog}>
                    <DialogTitle>Dialog Sample</DialogTitle>
                    <DialogContent>
                        <DialogContentText>
                            Easy to use Material UI Dialog.
                        </DialogContentText>
                    </DialogContent>
                    <DialogActions>
                        <Button
                            color="primary"
                            onClick={this.handleCloseDialog}
                        >
                            OK
                        </Button>
                    </DialogActions>
                </Dialog>
                <AppBar>
                    <Toolbar>
                        <Typography variant="h6" color="inherit">
                            TypeScript + Next.js + Material UI Sample
                        </Typography>
                    </Toolbar>
                </AppBar>
                <Typography variant="display1" gutterBottom={true}>
                    Material-UI
                </Typography>
                <Typography variant="subheading" gutterBottom={true}>
                    example project
                </Typography>
                <Typography gutterBottom={true}>
                    <Link href="/about">
                        <a>Go to the about page</a>
                    </Link>
                </Typography>
                <Button
                    variant="contained"
                    color="secondary"
                    onClick={this.handleClickShowDialog}
                >
                    Shot Dialog
                </Button>
                <style jsx={true}>{`
                    .root {
                        text-align: center;
                    }
                `}</style>
            </div>
        );
    }
}

export default withStyles(styles)(Index);

まずは、ReactのコンポーネントをTypeScriptで書くためのPropsやStateの型定義の渡し方ですね。Componentのパラメータとしてtypeを設定します。やっかいなのは、Material UIのスタイル用の機能です。テーマを元に少し手を加えればできる、という仕組みが実現されていますが、TypeScriptでやるには少々骨が折れます。それがstyles関数とwithStyles(styles)の部分です。

Screen Shot 2018-11-27 at 2.20.50.png
Screen Shot 2018-11-27 at 2.20.56.png

まとめと、普段の開発

これで一通り、Reactを使う環境ができました。BFF側にAPI機能を持たせたいとか、Reduxを使いたい、というのがあればここからまた少し手を加える必要があるでしょう。

開発はnpm run devで開発サーバーが起動し、ローカルのファイルの変更を見てホットデプロイとリロードを行ってくれます。

デプロイ時はnpm run buildとすると、.nextフォルダ内にコンテンツが生成されます。npm run buildの後に、npm run exportをすると、静的ファイルを生成することもできます。ただし、いくつか制約があったりしますので、ドキュメントをよくご覧ください。

Reactも、ここまでくればそんなに難しくないですよ。

Reactコンポーネント開発

Storybookが人気ですが、普段の開発のReact以外に、Storybook専用の記述を覚えたりメンテナンスをする、というのが(予想通り)あまり浸透しなかったので、まあ、本番アプリと同じNext.jsで環境を作ってみます。Angularのライブラリの開発体験が良かったので、それをTypeScript + Reactでもやってみよう、という魂胆です。

コンポーネント開発の場合、それをブラウザで見てみる環境は絶対必要ですが、それをどう整備するかが問題です。一応それを専門としたStorybookがありますし、GatsbyのようなReactを使った静的なドキュメントツールも使えなくもないでしょう。ただ、TypeScriptを使おうとしたときに、それらのツールの使っているBabelとかとの親和性というと一気に厳しくなります。

複数の環境を用意して理解してメンテするのは辛いのでプロダクション環境と同じ方が最終的に負担が少ないということがわかったので、上記のNext.jsの環境をそのまま利用します。まずは、上記のReactのウェブ環境をそのまま構築します。はいできましたね、次に行きます。

作業フォルダを作る

作るコンポーネントはReactのアイコンを表示するreact-iconとします。

基本のフォルダ構造としては次の構成で進めます。Next.jsのプロジェクトに、monorepoスタイルで、サブプロジェクトがいる、という構成です。サブプロジェクトはprojectsというフォルダの中にいます(名前はBabelじゃなくてAngularに準拠しています)。

+- dist           : ビルド済みのサブプロジェクトが格納される場所
|   +- react-icon : ビルド済みのライブラリ。ここの中でnpm publishしてnpmにアップロード
+- pages          : サンプルのページが入るところ
+- src            : サンプルで使う小物ファイルが入るところ
+- projects       : サブプロジェクトが入るところ
    +- react-icon : 今回作るライブラリ
        +- src    : ライブラリのソース

ビルドのツールのインストールと設定

tsconfig.jsonは、ルートのサンプルのアプリケーション用と、ライブラリ用、その共通部分の3つ作ります。まず共通分です。

tsconfig.common.json
{
  "compilerOptions": {
    "allowSyntheticDefaultImports": true,
    "baseUrl": ".",
    "jsx": "preserve",
    "lib": ["dom", "es2017"],
    "module": "esnext",
    "moduleResolution": "node",
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "preserveConstEnums": true,
    "removeComments": false,
    "skipLibCheck": true,
    "sourceMap": true,
    "strict": true,
    "target": "esnext"
  }
}

ルートのアプリケーションはこの共通部分をextendsで読み込みつつ、差分のみが追加されたjsonを作ります。webpack経由でTypeScriptが呼ばれるので、おなじみのnoEmit: trueですね。Next.jsはJSコードもあるのでallowJs: trueにします。

tsconfig.json
{
  "extends": "./tsconfig.common.json",
  "compilerOptions": {
    "noEmit": true,
    "allowJs": true,
    "baseUrl": "./"
  }
}

Reactのライブラリなので、jsxのオプションを入れます。変換しないといけないため、"preserve"ではなくて"react"にします。出力先はさきほど作ったdistフォルダの以下に出力するようにoutDirの設定もしています。型定義も出力します。

projects/react-icon/tsconfig.json
{
    "extends": "../../tsconfig.common.json",
    "compilerOptions": {
        "jsx": "react",
        "outDir": "../../dist/react-icon",
        "declaration": true
    },
    "include": ["src"]
}

package.jsonやREADMEなど、ファイルをアップロードするときに使いたい設定ファイルを用意しますが、monorepoの流儀に従って、projects/react-iconに入れて、ビルド時にdistにコピーするようにします。最初からdistに入れておいても良いのですが、成果物フォルダにソースが入っているのはちょっと気持ち悪いですよね。コピーのためのパッケージを入れておきます。あとはタスクがちょっと複雑なのでnpm-run-allも入れておきます。静的HTMLのサーバ、http-serverも入れます。

$ npm install cpx npm-run-all http-server

ルートのpackage.jsonでscriptsはこんな感じです。ちょっと、複雑ですが、大きくは、build, dev, export, prodあたりです。Next.jsを使うメリットとしては、静的HTMLのエクスポートが可能な点があります。そのままライブラリのドキュメントとしてjs.orgとか、github pagesとかにアップロードできます。

  • build: ライブラリをビルドして、dist/を更新
  • dev: サンプルのページのプロジェクトのウェブを表示&ホットリロード
  • export: 静的HTMLを生成
  • prod: 静的HTMLを生成しつつ、ローカルのHTMLサーバーで表示
package.json
  "scripts": {
    "build": "npm-run-all -p lib:build lib:copy1 lib:copy2  lib:copy3",
    "lib:build": "tsc -p projects/react-icon",
    "lib:copy1": "cpx \"projects/react-icon/package.json\" \"dist/react-icon\"",
    "lib:copy2": "cpx \"projects/react-icon/*.md\" \"dist/react-icon\"",
    "lib:copy3": "cpx \"projects/react-icon/.npmignore\" \"dist/react-icon\"",
    "dev": "next",
    "export": "npm-run-all -s build doc:build doc:export",
    "doc:build": "next build",
    "doc:export": "next export",
    "doc:server": "http-server out -o",
    "prod": "npm-run-all -s export doc:server",
    "prepack": "npm run build",
    "pack": "cd dist/react-icon && npm pack"
  }

まとめと、普段の開発

あとは、projects以下のライブラリのコードを修正しつつ、ビルドしてからサンプルを更新して・・・みたいな感じで開発ができます。ビルドしたあとは、dist/(ライブラリ名)のフォルダの中で、npm publishすると、ライブラリをアップロードできたりします。サンプルは本番環境と近い、通常のReactプロジェクト(Next.jsプロジェクト)の中でコードが書けます。Reduxと繋いでみたりとか、実践的なサンプルもできます。

おまけの追加ツール

react-sample-page-generatorというツールを作りました。

pagesを自分でつくる方式だと、ページ間のナビゲーションは自前で実装しなければなりません。そのあたりを自動化するだけのツールです。次のようなフォルダ構造を想定しています。

+- dist
|   +- react-icon
+- pages
+- src
+- projects
|   +- react-icon
|       +- src
+- samples        : これ!

react-sample-page-generatorは、サンプルのファイルのリストを元に、それぞれのファイル間で移動できるナビゲーションを追加しつつNext.jsで使えるページのファイルにしてpagesにファイルを生成します。ついでに、マークダウンのファイルもレンダリングして表示するページをpagesに作成します。

pakcage.json
"scripts": {
  "build": "npm-run-all -p lib:build doc:update lib:copy1 lib:copy2  lib:copy3",
  "doc:update": "react-sample-page-generator -p projects/react-icon"
}

サンプルの場合はソースコードをハイライトしていれたりとか、ちょっとおまけの処理もしていたりします。

Screen Shot 2018-11-28 at 19.02.05.png

その他

  • ウェブはウェブでも、Angularは最初からTypeScriptでハッピーでご機嫌です。環境構築に強いメンバーがいなくて、フレームワーク何にしようかな?という段階であれば、Angularを選んでおけばいいのかなって思います。最近僕はMithirl以外だとAngular派になりました。
  • 現代にもなって、未だにES5はおろか、3にもなってなくて、ArrayのforEachですらエラーになるGoogle Apps Scriptですが、claspを使えば、ローカルでTypeScriptでコードが書けます。ES3で出力してくれるので、かなりストレスが軽減されるでしょう。

まとめのまとめ

いろいろ書きましたが、一番書きたかったのは、最後のReact Componentですね。

  1. 趣味のツールを作りたい
  2. TypeScript + Reactでやってみたい→表が欲しい
  3. CheetahGridがReactに対応していない
  4. CheetahGridの型定義つきのReactコンポーネント作ろう
  5. Reactコンポーネント開発の環境を作ろう→いろいろ面倒なのでツールも作ろう
  6. そもそもTypeScriptのライブラリを公開するのってどうするんだろう?

という、2ヶ月近くに渡る壮大なyak shavingのドキュメンタリーでした。ビルドエンジニアという専任のお仕事があるように、環境構築は意思決定も含めてきちんと書き出すとかなりの分量になりますね。比較的古めのツールやライブラリで固めています。

まあ、脱Babelとは言いますが、Babelそのものは価値があって、未知の文法を実装してみんなで試すプラットフォームになっていて、言語の発展に寄与しているのは尊いです。ただ、それを隠そうとしてかえって難しくなっている各種ツールとかが難易度を高くしているだけで、そんなには嫌いじゃないです。とりあえず今は成果を出す方にフォーカスして、Babelは老後の趣味に取っておきます。

明日は @laqiiz さんです。