朝日ネット 技術者ブログ

朝日ネットのエンジニアによるリレーブログ。今、自分が一番気になるテーマで書きます。

TypeScript + React + Redux + α の利用例兼チートシート

追記 (2018/12/25)

当記事をリニューアルし、より詳細な解説を加えた続編を書きました。 今後はこちらの記事をご参照いただければと思います。

techblog.asahi-net.co.jp

はじめに

開発部の tasaki です。 Web フロントエンド(というより Node.js + npm)のエコシステムでは他のエコシステムと比較して大量の細かいパッケージを作成する傾向にあります。 TypeScript + React + Redux + react-redux + react-router + redux-form + ... などとを組み合わせていった場合、各パッケージがどのような機能を持ち、大量のパッケージのどこに関数やクラス(+ TypeScript の型)が定義されているのかよく分からなくなりがちです。 そこで、ソースコードをそのままチートシートとして使えるような小さいアプリケーションを作成してみました。

ソースコード

  • TypeScript + React + Redux に react-redux, react-router, redux-form, recompose, reselect などを組み合わせています
    • 詳細については下記の package.json を参照してください
  • 関数の所属が分かりやすいようにあえて import * as Foo from "foo"; (いわゆる qualified import)の形でパッケージをインポートしています
  • tsx + html + package.json + tsconfig.json の 4 ファイルのみで完結しており Parcel でのバンドルが可能です
  • 以下のコマンドでサーバを立ち上げて実行できます
git clone https://gist.github.com/763f811e02a9025d0126dcef55376b31 url-timer
cd url-timer
yarn  # or npm update -D
npx parcel --public-url /url-timer/ index.html
  • 例えば http://localhost:1234/url-timer/2020-07-24T20%3A00%3A00%2B09%3A00 にアクセスすると 2020-07-24T20:00:00+09:00 までの残り時間がリアルタイム表示されます
    • http://localhost:1234/url-timer/0 にアクセスすると UNIX エポック (1970-01-01T00:00:00.000Z) からの経過時間が表示されます

gist.github.com

備考

Parcel を使う

このような小規模なソースコードに対してはほぼ設定不要で使える・バンドル時間も短い Parcel を使うことをおすすめします。 必要が生じてから Webpack 等に移行すればよいでしょう(逆に Webpack → Parcel の移行はよほどシンプルなプロジェクトでない限り無理かと思われます)。 また、TypeScript + Webpack を使う場合の構成については create-react-app --scripts-version=react-scripts-ts したプロジェクトを即 npm run eject したものが非常に参考になります。

Parcel は TypeScript の型エラーを報告してくれない

Parcel は script タグで ts, tsx ファイルを src すると TypeScript のコンパイラを自動で呼び出してくれますが、parcel serveparcel build をしても型エラーのレポートを出してくれません。 エラーチェックは Visual Studio Code などのエディタ側でやる、parcel servetsc -w の二段構成にする(html からは tsc によるコンパイル後の js を src する)などの工夫が必要です。

props の定義に注意

TypeScript は従来の JavaScript ライブラリの複雑怪奇な型に対応するために比較的表現力の高い型システムを持っていますが、 それにも限度はあり人間が努力を怠ると容易に型による恩恵を受けられなくなってしまいます。

例えば react-redux の connect 関数、redux-form の reduxForm 関数、react-router の withRouter 関数などによって注入される props については基本的に TypeScript の型推論は効きません。 よって手動で props のための interface(上記ソースコードの OwnProps, StateProps, DispatchProps など)を作ってジェネリクスの引数として渡す必要があるのですが、この定義を間違えると大抵の場合実行時エラーになってしまいます。 props の型を定義する際には特に細心の注意を払いましょう。

循環依存に注意

TypeScript モジュール間で相互に import をしてもコンパイル時にエラーにならない場合があり、 そうなってしまった場合非常に分かりにくい実行時エラーが発生します。 というより、実行時エラーが発生するならまだ良い方で、以下の例ではエラーは出ず undefined が文字列化されて出力されてしまいます。

// foo.ts
import { bar } from './bar'

export const foo: string = "hello " + bar;
// bar.ts
import { foo } from './foo'

export const bar: string = foo + " world";
// index.ts
import { foo } from './foo'
import { bar } from './bar'

console.log(foo);
console.log(bar);
$ tsc --strict src/index.ts
$ node src/index.js
hello undefined world
undefined world

このような事故を防ぐために「定数やユーティリティ関数を集めたファイル・interface の定義ファイルは別モジュールに分け、それらは他のモジュールには依存させない」「container は store や reducer に依存させるが逆は絶対にしない」などの規約をしっかり定めたうえで開発を進めていく必要があります。循環依存を検知する eslint / tslint plugin を使うのも手です。

おわりに

今回はソースコード自体をチートシートとして使えるような TypeScript + React + Redux + α を使った小さな Web アプリケーションを作りました。

こういうアプリケーションは今だと Vue.js + vuex + vue-router で作るのが普通かもしれません。 今回の構成ではこのシンプルなアプリケーションでもメインファイルがコメント等を含めて 300 行ほどになりました1。 ただ、Props さえ正しく定義して Visual Studio Code を使えば型推論と補完が効くので書き心地としては行数の割に良い感じだと思います。

採用情報

朝日ネットでは新卒採用・キャリア採用を行っております。


  1. tsc で JavaScript (ES5) にコンパイルすると 200 行程度。