tsconfig.jsonでstrict: trueとするのは時期尚早だった
できるだけ厳しめ環境で書いた方がよろしかろうと、TypeScriptのトランスパイラオプションを最もstrictにしましたが、私の現時点でtsconfig.jsonで、strict: trueとするのは時期尚早でした。
- tsconfig.jsonで、strict: trueとする
- webpack.config.*.tsで、'path'と'webpack'をimportしているところで型定義が無いとエラーがでる
- yarn add @types/webpack -Dとして型定義を追加する。dependenciesで@types/nodeが入る
- @types/webpack-envと@types/nodeで、Moduleの定義がぶつかり、死ぬ。
- @types/webpack-envは、HMRでmodule.hotを呼ぶために必要。
- @types/webpackを抜く。yarn remove @types/webpack
- ここでワークアラウンド二択
- tsconfig.jsonから、strict: trueを抜く、もしくは
- webpack.config.*.tsを、TypeScriptやめて普通のJavaScript(.js)にする。
当面の結論
@types/nodeと@types/webpack-envの激突の根本はグローバルスコープの競合のため。この解決は当事者間の折り合いを待つか、より根本解決として将来のTypeScriptでグローバルスコープの素敵ソリューションが提供されるのを期待するかというとこかなと。ワークアラウンドとして、webpack.configをJavaScriptにします。美学だけで、効能だけ考えれば無理にTypeScriptで書く必要性なかった。そして、yarn remove ts-node。tsconfig.jsonのstrict: trueはがんばって残す。
よってこの記事のタイトルは、正しくは「webpackの設定をTypeScriptにするのは時期尚早だった」。
Redux DevToolsの問題
ChromeのRedux DevToolsも、ワークアラウンド。yarn add redux-devtools-extension。
import { createStore, combineReducers, applyMiddleware } from 'redux'; import { routerReducer as router, routerMiddleware } from 'react-router-redux'; import { History } from 'history'; import { composeWithDevTools } from 'redux-devtools-extension'; import { app } from './AppReducer'; import { pushMiddleware } from './pushMiddleware'; export default function (history: History) { const store = createStore( combineReducers({ router, app }), composeWithDevTools( applyMiddleware( routerMiddleware(history), pushMiddleware(), ), ), ); if (module.hot) { module.hot.accept('./AppReducer.ts', () => { store.replaceReducer(combineReducers({ router, app: require<{app: typeof app}>('./AppReducer').app }, )); }); } return store; }
型情報がしっかり提供されていて驚いた。
Reduxでの有用なTypeScriptサンプル
https://github.com/reactjs/redux/tree/master/test/typescript
Reduxのリポジトリの中、test/typescriptにReduxでの有用なTypeScriptサンプルがあります。強い型付けでのMiddlewareの書き方とか参考になるもの多い。また、これらのテストは動かさずにコードをコンパイラやLintにかけることでindex.d.tsをテストするという。こういうテストもあるのだなあ。
Deep Dive
Introduction · TypeScript Deep Dive
一冊まるごとオンラインで。著者の人は何かに困ってググると、各掲示板でたくさんの人に答えてるすごい人。PDF版もあった。
型強化の呪文。noImplicitAny
上記記事をたまたま読んで目にした呪文。noImplicitAny。
Over time, they reached the point of enabling advanced compiler options, such as –noImplicitAny to prevent the compiler from inferring an any type.
advancedってすごいな。なるほど。やってみましょう。また、ドキュメントを見ると、strictNullChecksなるものも。
// tsconfig.json { "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true, "module": "commonjs", "target": "es5", "jsx": "react", "lib": ["es6", "dom"], "types": ["webpack-env", "jest"] } }
ちょいちょい「暗黙のany」を怒るエラーがでてきました。たとえばこんなの。
export class Counter extends React.Component<{}, { counter: number }> { constructor(props) { super(props); this.state = { counter: 0 }; } // 省略 }
このconstructorの引数に型が無い。直すと以下。
export class Counter extends React.Component<{}, { counter: number }> { constructor(props: {}) { super(props); this.state = { counter: 0 }; } // 省略 }
結構な箇所を直したら、noImplicitAny=trueでもOKになった。明示的なanyはOK。
import { handleActions } from 'redux-actions'; import { LOCATION_CHANGE } from 'react-router-redux'; import { matchPath } from 'react-router'; import { Map } from 'immutable'; const decodeName = (pathname: string) => { const m = matchPath<{ name?: string; }>(pathname, { path: '/player/:name' }); if (m) { return { ...m.params, name: m.params.name }; } return {}; }; type AppState = Map<string, any>; export const app = handleActions<AppState, any>( { // 省略 MOVE_TO: (state: AppState, { payload: { name } }) => { return state.merge({ name }); }, [LOCATION_CHANGE]: (state: AppState, { payload: { pathname } }) => state.merge(decodeName(pathname)), }, Map({}), );
Redux ActionsのhandleActionsの型引数の2番目には、Payloadがきます。でもこれはMOVE_TOのときには、{ name } だし、LOCATION_CHANGEのときは{ pathname }だから、anyで受けたかった。で、明示しているからOKだった。
React RouterのmatchPathは面白くて、型引数にExpressスタイルのURL分解パラメータをあらかじめ型として持つ。'/player/:name'の結果を{ name?: string; }で受けたために、m.params.nameという取り方ができるようになる。
strictNullChecksは結構直すの簡単ながら、なるほどそこでnullの可能性あるかと気がつかしてくれる。とても有用。
結論
noImplicitAnyはtrueにするのが正義。strictNullChecksはさらにもっと実利がある。ほか、noImplicitThisやalwaysStrictも含めて、単にstrict=trueにすれば一番厳しい。
TypeScriptでもリリースビルドする
TypeScriptの8つめ。
Productionをやります。
// webpack.config.production.ts import * as path from 'path'; import * as webpack from 'webpack'; export default { entry: './ts/index.tsx', output: { path: path.resolve('static/js'), filename: 'bundle.js', }, module: { rules: [{ test: /\.(ts|tsx)$/, use: [ 'awesome-typescript-loader', 'tslint-loader', ], }], }, resolve: { extensions: ['.ts', '.tsx', '.js'], }, plugins: [ new webpack.optimize.UglifyJsPlugin({ sourceMap: false, comments: false }), ], };
特に論点ないですね。
$ yarn add rimraf -D
rimrafをいつも使ってます。
// package.jsonの一部 "scripts": { "build": "rimraf ./static/js/*; webpack -p --config webpack.config.production.ts" },
yarn run buildもショートハンドがあります。これはnpmと同じ。
$ yarn build
Atomを入れてTypeScriptを書く
TypeScriptの7つめ。
愛用のWebStormでTypeScriptを研究しつつ、おまけでAtomをインスコしてサンプルを作るときの清書プラットフォームとして使って見ました。
私はHomebrewでNodeもYarnも入れてますので、Atomも。MacOS向けGUIインストーラーアプリをHomebrewでいれる拡張として、このHomebrew Caskを使います。
$ brew tap caskroom/cask $ brew cask install atom
プラグインをガシガシいれます。入れたのは以下のものと、それらが依存するもの。
他にはちょこちょこ入れ出ししてみたけど、良いものが見つからず、上のでほぼいい感じ。
課題
「Go to Declaration」で型定義に飛んだりすることができなかった。調べるとバグなんだか、そうじゃないんだか。結構長い間苦労されているのかな?Atomでまだ解決できてない。
しかし、WebStormはザクザク型定義で飛んだり跳ねたりできる!すげーや商用(なのかな?)。
$ brew cask uninstall atom $ rm -rf ~/.atom
一旦、Atomはアンインストール。また時機をみて試してみます。おそらくWebStormのサブスクリプションが切れるころ。
TypeScriptでもReduxのHMR対応しておきましょう
TypeScriptの6つめ。
HMRには3段階あって、非Reactアプリ、Reactアプリと続いて最後にReact+Reduxアプリに到達します。
// /store/Store.ts import { createStore, combineReducers, applyMiddleware } from 'redux'; import { routerReducer as router, routerMiddleware } from 'react-router-redux'; import { History } from 'history'; import { composeWithDevTools } from 'redux-devtools-extension'; import { app } from './AppReducer'; export default (history: History) => { const store = createStore( combineReducers({ router, app }), composeWithDevTools( applyMiddleware( routerMiddleware(history), ), ), ); if (module.hot) { module.hot.accept('./AppReducer.ts', () => { const nextReducer = { router, app: require<{app: typeof app}>('./AppReducer').app }; store.replaceReducer(combineReducers(nextReducer)); }); } return store; };
もう派生論点なんでフルにコードは示さずとも、ポイントだけ。requireを使って更新Reducerを別名で読み込んで、store.replaceReducerを呼ぶことです。
上記コードではChromeプラグインのRedux DevToolsに対応させるため、react-router-reduxを入れました。
TypeScriptでもHMRを実現する
TypeScriptの5つめ。まだまだ続きます。
$ yarn add react-hot-loader@next
$ yarn add @types/webpack-env -D
react-hot-loaderは3.0.0-beta.7が入りました。そして@types/webpack-envを!これが自分的には一番ヤバい。見つけるのに苦労した。
// webpack.config.development.ts import * as path from 'path'; import * as webpack from 'webpack'; export default { entry: [ 'react-hot-loader/patch', './ts/index.tsx', ], output: { filename: 'bundle.js', publicPath: '/js/', }, devtool: 'inline-source-map', module: { rules: [{ test: /\.(ts|tsx)$/, use: [ 'react-hot-loader/webpack', 'awesome-typescript-loader', 'tslint-loader', ], }], }, resolve: { extensions: ['.ts', '.tsx', '.js'], }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NamedModulesPlugin(), new webpack.NoEmitOnErrorsPlugin(), ], devServer: { host: 'localhost', port: 9000, hot: true, historyApiFallback: true, contentBase: path.resolve('static'), }, };
いろいろ出し入れしてみて、一番シンプルな作りがこれかなあ。historyApiFallbackは今は必要ないですが、React RouterすなわちHistoryを用いてかつProxyでワークアラウンドしないアプリを作るときに必須です。
- entryにreact-hot-loader/patchを入れる
- ローダーチェーンの先頭にreact-hot-loader/webpackを入れる
- HotModuleReplacementPluginを登録する
- devServer.hotをtrueにする
// tsconfig.json { "compilerOptions": { "module": "commonjs", "target": "es5", "jsx": "react", "lib": ["es6", "dom"], "types": ["webpack-env", "jest"] } }
ここで、webpack-envが出てくる!これが無いとHMRの定石である、module.hotの名前解決ができない。いやーグローバル名前空間でいろいろやれるのはダメだと思いますが、JavaScriptの黒歴史としてこんなのが山ほどこれからも出てくることでしょう。TypeScriptはJavaScriptとそのまま地続きだからグローバルで自由なユーザー定義を禁止するような言語仕様にできなかったか。。。
// /components/App.tsx import * as React from 'react'; import { Hello } from './Hello'; import { Counter } from './Counter'; export const App = () => ( <div> <Hello compiler="TypeScript" framework="React" /> <Counter /> </div> ); // /index.tsx import * as React from 'react'; import * as ReactDOM from 'react-dom'; import { AppContainer } from 'react-hot-loader'; import { App } from './components/App'; const render = () => ReactDOM.render( <AppContainer> <App /> </AppContainer>, document.getElementById('application'), ); render(); if (module.hot) { module.hot.accept('./components/App', () => { const NextApp = require<{App: typeof App}>('./components/App').App; ReactDOM.render( <AppContainer><NextApp /></AppContainer>, document.getElementById('application'), ); }); }
Hotにする対象として、App.tsxに受け直しておきます。これをmodule.hot.acceptを用いて監視するのですが、ES6でやったときのようにWebpack2以降のモジュール読み込みシステムなるものは使えない。tsconfig.jsonのほうでモジュールシステムをcommonjsにしているのでこうなったかなと推測してます。かといってcommonjs以外の選択肢はTypeScriptのコンパイラオプションのドキュメント読んでいろいろ研究して見たけどどうもぴったりの解がほかになかったです。
requireを使ってNextAppと別名で読み込み直すのはコードもトリッキーな印象のものになります。型を解決するために、require<{App: typeof App}>なんて書き方してますからね。もしAppをexport defaultしていると、require<default: typeof App}>(‘./components/App’).default; なんてことになってエディタによっては「defaultは予約語だ!」というようにLintで引っかかったりします。汚いが、しょうがない。いまのところ他にやり方が思いつかない。
これで、TypeScriptでReactのHMRもできました!