TL; DR.
@babel/preset-typescript
があるので ts-loader
(webpackのTypeScript用プラグイン)はいらない子になったと言いたいです。のでbabel-loader+webpackでTypeScriptを使ったreactの開発環境を作るのが今回のゴールです。ネタバレするとトランスパイルはbabel-loader+webpackで問題ないですが、型チェックが必要なのでtscで型チェックします。
Installing
$ npm install --save-dev \
@babel/cli \
@babel/core \
@babel/preset-env \
@babel/preset-react \
@babel/preset-typescript \
@types/react \
@types/react-dom \
babel-loader \
typescript \
webpack \
webpack-cli
$ npm install --save \
react \
react-dom
babelの設定
そもそもコヤツがMicroSoftのブログでpublishされたのが2018年8月27日。
ref: https://blogs.msdn.microsoft.com/typescript/2018/08/27/typescript-and-babel-7/
@babel/preset-typescript
と@babel/preset-env
を使えばTypeScript->任意のversionのESに変換してくれます。
// babel.config.js
'use strict';
const presets = [
// 必要に応じて browserslist(対象のブラウザ) とか useBuiltIns (polyfill) の設定を入れていこうな
// ['@babel/preset-env', {browserslist: '> 0.25%, not dead'}]的な
['@babel/preset-env'],
['@babel/preset-typescript', {
// 強制的にjsxのパースを行うオプション。
// e.g: var hoge = <string>fuga; みたいなコードがパースできる
isTSX: true,
// isTSX: trueにするときは常に必須のオプション
allExtensions: true
}],
['@babel/preset-react', {
// WIP: 後半でここ、NODE_ENVで切り替えられるように変更します
development: true
}]
];
module.exports = {presets}
まずはこれだけでbundleはされませんが、
// src/Index.tsx
import * as React from 'react';
import {render} from 'react-dom';
render(
<h1>Hello World!!</h1>,
document.querySelector('#root')
);
上記のようなコードが書けるようになります。んでもって
$ babel src/Index.tsx -o dist/index.js
こうすれば、babelによるコードのトランスパイルは完了。
webpackの設定
次はjsをbundleして単体のファイルで動くようにしていきます。っつてもここは普段のwebpackの設定とそんなに変わりません。
// webpack.config.js
'use strict';
const path = require('path');
module.exports = {
target: 'web',
// entry pointをrepository root からの src/Index.tsxを想定
context: path.join(__dirname, 'src'),
entry: './Index',
// 出力先は dist/index.js です
output: {
path: path.join(__dirname, 'dist'),
filename: './index.js'
},
resolve: {
extensions: ['.tsx', '.ts', '.js']
},
module: {
rules: [{
test: /\.tsx?$/,
exclude: /node_modules/,
loader: 'babel-loader',
}]
}
};
ここは皆さんの普段のbabel-loader+webpackと変わらんのジャマイカでしょうか。
型チェックの問題
さて実はここで静的型チェックの問題があります。
例えば
// src/Index.tsx
import * as React from 'react';
import {render} from 'react-dom';
// number型にstringを入れてる
const content: number = 'Hello World!!';
render(
<h1>{content}</h1>,
document.querySelector('#root')
);
number型にstringを入れてるの明らかに間違ってるんですが、@babel/preset-typescript
では型チェックをしないので(トランスパイルするだけ)、これは通っちゃってdist/index.jsに成果物が吐き出されます。のでトランスパイルはbabelを使い、静的型チェックは従来どおりtscを使うという戦略で行きます。
型チェックとしてのtscの導入
まずはtsconfig.jsonをば。
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"allowJs": false,
"jsx": "react",
"declaration": false,
"noEmit": true,
"strict": true
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
]
}
意図としては、コードのトランスパイルはbabelがやってくれるので型チェック(厳密にはimport先が間違ってないかとかも)だけやってtscコマンドを実行した後は何も生成しないという意図になります。んでは一個ずつオプションの解説をば。
target
コンパイル後のEcmaScriptのバージョン指定です。
実際はtscで成果物作るわけではないのでなんでもいいと思いますが、一応バージョン固定したかった(のでESNEXT
は指定してません)のと、現時点(2019年2月)で最新のes2018
を指定しました。
module
moduleのimport方式をどうするか。ここも成果物を作るわけではないので、なんでもいいと思います。
allowJs
tsxとtsしか使わないよという前提であれば(import先を除く)でfalseにしてCIフェーズとかで弾いてもいいのではと考えました。
declaration
falseにすることでd.ts
の型定義ファイルの生成をさせないようにします。
noEmit
trueにすることで置換後のjsファイルを吐き出さないようにします。(src/Index.tsxに対してsrc/Index.jsみたいな)
strict
trueにすることで、nullとかanyとかimplicit returnを厳格にチェックしてくれます。
react.production.min.jsをバンドルに使う
@babel/preset-react
にはdevelopment
というオプション(ref: https://babeljs.io/docs/en/babel-preset-react#development)があって、これの切り替えによってdevelopmentモードをtrueにできます。また、react/index.js
の配下には
// node_modules/react/index.js
'use strict';
if (process.env.NODE_ENV === 'production') {
module.exports = require('./cjs/react.production.min.js');
} else {
module.exports = require('./cjs/react.development.js');
}
というコードが入っていて、環境変数NODE_ENVによる切り替えが可能です。ので、ここではbabel.config.jsでNODE_ENVをコントロールできるように
// babel.config.js
'use strict';
// NODE_ENVがproductionかどうかの判定
const isDev = process.env.NODE_ENV !== 'production'
const presets = [
['@babel/preset-env'],
['@babel/preset-typescript', {
isTSX: true,
allExtensions: true
}],
['@babel/preset-react', {
development: isDev
}]
];
module.exports = {presets}
NODE_ENVの判定を入れました。
仕上げ
さて、これでビルドの材料は揃ったので、仕上げにnpm-scripts
を仕込んでいきましょう。
以下はpackage.jsonのscripts部分の抜粋です。
"scripts": {
"build:production": "NODE_ENV=production; tsc && webpack --mode=production",
"build:development": "tsc && webpack --mode=development"
}
例えばですが、ガンガン開発するときはtsc無視してciとかテストフェーズだけtsc回して型ミスってるところだけ直していくっていうスタイルも取れるのかなーと考えています。
(まぁparcel.js使えと言われれば元も子もないんですけどねw)