Edited at

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


TL;DR

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

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



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

2019/1/22: TSLint→ESLintに修正

2019/8/1: Next.js@9がTypeScriptにデフォルト対応したので説明を修正


まえがき

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

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、ESLint、Prettierなどを選択しています。まあ、どれも相性問題が出にくい、数年前から安定して存在している、公式で推奨といった保守的な理由ですね。きちんと選べば、「JSはいつも変わっている」とは距離を置くことができます。



  • Jest

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




  • ESLint

    TSLintが今まで多く使われていましたが、Microsoftが、ESLintで今後やっていく旨を発表しています。今日、ちょうど1.0がリリースされました。TSLintでは構造的にパフォーマンスが出しにくいとのことでした。あとTSLintはデフォルトの設定が少し癖が強かった

    @typescript-eslint/eslint-plugin

    ESLint/TSLintをバックエンドで使い、他のツールとの相性を考えたデフォルト設定(後述のPrettierとかぶるstyleの設定が全部抜いてある)が最初から入ったlyntというのもあり、上述のESLint対応がうまく進むならこれもありかと思います。




  • Prettier

    typescript-formatterもありますが、ESLintの--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
jest.config.json
.eslintrc
.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

eslint @typescript-eslint/eslint-plugin eslint-plugin-prettier eslint-config-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/**/*"] // 大事
}

ESLintの設定も作ります。Prettierと連携するようにします。@typescript-eslint/indentを無効にしないと、Prettierと喧嘩するので無視するように設定します。@ota-meshiさん情報によると、今修正中のようです。console.logぐらいは許可して欲しい。


.eslintrc

{

"plugins": [
"prettier"
],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended"
],
"rules": {
"no-console": 0,
"@typescript-eslint/indent": 0,
"prettier/prettier": 2
}
}

2019/01/31: eslint-config-prettierの4.0.0が出たので、indentの設定はもういらないかもしれません。 tnx @ota-meshi さん

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 build:esm",
"build:cjs": "tsc --project . --module commonjs --outDir ./dist-cjs",
"build:esm": "tsc --project . --module es2015 --outDir ./dist-esm",
"lint": "eslint .",
"fix": "eslint --fix ."
}


テスト

ユニットテスト環境も作ります。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でフォルダを開いたときに、eslintの拡張と、editorconfigの拡張がインストールされるようにします。


.vscode/extensions.json

{

"recommendations": [
"dbaeumer.vscode-eslint",
"EditorConfig.editorconfig"
]
}

ファイル保存時にeslint --fixが自動実行されるように設定しておきましょう。これでVisual Studio Codeを使う限り、誰がプロジェクトを開いてもコードスタイルが保たれます。eslintプラグインのautoFixOnSaveは、files.autoSaveがoffのときにしか効かないので、それも設定しておきます。


.vscode/settings.json

{

"eslint.autoFixOnSave": true,
"files.autoSave": "off"
}


ライブラリコード

これまでの設定ファイルは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 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
eslint @typescript-eslint/eslint-plugin eslint-plugin-prettier eslint-config-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": "eslint .",
"fix": "eslint --fix ."
}

テストの設定、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のバージョン9からはデフォルトでTypeScriptに対応しているため、追加でインストールしたり設定ファイルをいじる必要はありません。

Next.jsは大きすぎる、自分でいろいろやりたい、という方はcreate-react-appに--typescriptオプションをつけて実行すると環境を作ってくれます。

npx create-react-app myapp --typescript


作業フォルダを作る

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をインストールします。他にも必要なものを入れてしまいましょう。Next.jsの型定義ファイルは最初から内蔵なので別途入れる必要はありません。ReactのJSXに対応させるために、eslint-plugin-reactを忘れないようにしましょう。

# フレームワークと必須の依存

$ npm install next react react-dom typescript
# フレームワークと必須の依存の型定義(Next.jsは内蔵しているので不要)
$ npm install --save-dev @types/node @types/react @types/react-dom
# ツール類
$ npm install --save-dev prettier
eslint @typescript-eslint/eslint-plugin eslint-plugin-prettier eslint-config-prettier eslint-plugin-react npm-run-all
# テスト関係
$ npm install --save-dev jest ts-jest @types/jest

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

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

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

$ npm install @material-ui/core @material-ui/icons react-jss

tsconfig.jsonはNext.jsが初回実行時に自動で作ってくれるので不要です。Babelの設定も不要です。生成されるのは次のようなファイルです。ポイントとしては、JSXの文法はNext.jsはBabelが処理するので、JSX文法をそのままにしておくpreserveになっている点ですかね。Next.jsのコアはTypeScriptでリライトされつつあるということですが、そのうちallowJSもいらなくなるでしょう。

{

"compilerOptions": {
"target": "es5",
"lib": [
"dom",
"dom.iterable",
"esnext"
],
"allowJs": true,
"skipLibCheck": true,
"strict": false,
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "node",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve"
},
"exclude": [
"node_modules"
],
"include": [
"next-env.d.ts",
"**/*.ts",
"**/*.tsx"
]
}

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


next.config.js

const withSass = require("@zeit/next-sass");

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


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


package.json

  "scripts": {

"dev": "next",
"build": "next build",
"export": "next export",
"start": "next start",
"lint": "eslint .",
"fix": "eslint --fix .",
"test": "jest",
"watch": "jest --watchAll"
}

ESLintはJSX関連の設定や、.tsxや.jsxのコードがあったらJSXとして処理する必要があるため、これも設定に含めます。

あと、next.config.jsとかで一部Node.jsの機能をそのまま使うところがあって、CommonJSのrequireを有効にしてあげないとエラーになるので、そこも配慮します。


.eslintrc

{

"plugins": [
"prettier"
],
"extends": [
"plugin:@typescript-eslint/recommended",
"plugin:prettier/recommended",
"plugin:react/recommended"
],
"rules": {
"no-console": 0,
"prettier/prettier": 2,
"@typescript-eslint/no-var-requires": 0,
"@typescript-eslint/indent": 0,
"react/jsx-filename-extension": [1, {
"extensions": [".ts", ".tsx", ".js", ".jsx"]
}]
}
}


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 さんです。