Flow/PostCSS の大規模プロジェクトを TypeScript/emotion に移行して数万行のプルリクを投げた話

JX通信社CDOの小笠原(@yamitzky)です。

AI 緊急情報サービスの「FASTALERT」は、報道機関や公共機関に導入いただいている(お堅めな) BtoB SaaS でありながら、 事業開始当初から React を使った Single Page Application(SPA) として作っています。 2017年には、より信頼性のあるフロントエンドを提供するため、 Facebook の Flow を導入しました。しかし、昨今の TypeScript の盛り上がりや、社内の他プロダクトで TypeScript を使っていることなどを受けて、フロントエンドのアーキテクチャを大幅に見直しました。

今回取り組んだ大きな変更は、

  • Flow から TypeScript への移行 (型チェックの移行)
  • TypeScript 化に合わせた、 babel-plugin-proposal-*** の廃止 (文法の移行)
  • PostCSS から [emotion] への移行 (CSS の移行)

などです。

今回のブログでは大規模プロジェクトにおいて、Flow から TypeScript、PostCSS から emotion へ移行した際の勘所や、知見などをご紹介します。Flow を使ったことないけど、JavaScript から移行したい方にも役立つかと思います。

Flow と TypeScript

Flow は Facebook が中心に作っている JavaScript のための静的型チェッカーで、TypeScript は Microsoft が中心に作っている JavaScript の型付きな上位互換言語(superset)です。 Flow は JS への型宣言拡張、TypeScript はプログラミング言語と、微妙な立ち位置は違いますが、共通するモチベーションとして 「静的型付き言語でない JavaScript に型安全を持ち込む」 というものがあるかと思います。

また、両者の文法も似ている部分があり、下記は TypeScript でも Flow でも同じような挙動をするコードです。

// @flow
function square(n: number): number {
  return n * n
}
square("2") // Error!

2017年に技術選定した当時は TypeScript の方が盛り上がるとは思っていませんでした*1。2020年現在、TypeScript の方が GitHub 上のスター数やサードパーティーの型宣言も多く、React との相性や DX(開発者体験) も良いと感じています。

PostCSS と emotion

React などの SPA でデザインを実装していく際には、グローバルな CSS ではなくモジュール化された(コンポーネントに閉じた)CSSを使うことが望ましいです。FASTALERT ではそのために、PostCSS を利用していました。

PostCSS を使うことによって、

  • モジュール化された、グローバルを汚染しない CSS の実現
  • 自動的な vendor prefix
  • ネストした CSS のような新しい構文
  • 変数を活用した DRY な CSS 定義

などが実現できます。一方で、emotion でも同様のことは実現できます。emotion は CSS in JS と呼ばれるような派閥の一つで、JavaScript(TypeScript) 内に CSS を書くことができます。

そのため、「CSS の構文で書くことができる」という点で PostCSS と emotion は共通してるものの、定義方法は大きく異なります。

PostCSS版

/* index.css */
.square {
  width: 100px;
  height: 100px;
}
// index.tsx
import styles from './index.css'

const Component = () => <div className={styles.square}>四角形</div>

emotion版

// index.tsx
const Square = styled.div`
  width: 100px;
  height: 100px;
`
const Component = () => <Square>四角形</Square>

PostCSS では CSS に書く、emotion では JavaScript (TypeScript) 内に直接書いています。

emotion の良いところは、なんと言っても React x TypeScript の流儀で書くことができる ことです! CSS プロパティも型定義されていたりと、型の恩恵 が受けられます。同じ理由で、Styled System を使ってのデザインシステムの構築もしやすいです*2

移行方法

mizchi 氏の「非破壊 TypeScript」 を参考にしつつ、次のようなステップで行いました。ピックアップしてご紹介します。

  1. TypeScript 向けにライブラリのインストールや設定変更
  2. .js の拡張子を .ts(x) にして、 // @flow のコメントをなくす
  3. babel を走らせると怒られるので、 TypeScript 文法の誤りを地道に直す
  4. tsc --noEmit するとやっぱり怒られるので、TypeScript の型エラーが発生したところを地道に解決する
  5. 無理だったら諦めて as any.js
  6. .css を .tsx 内に ひたすらコピペ
  7. eslint で自動フォーマットを走らせて微調整

これらの作業に特に面白いツールなどは使っておらず、基本的には力技と、VSCode の正規表現による置換、そして TypeScript に怒られドリブンで進めました。気合があればできる と思います。

TypeScript 向けライブラリの設定

次のような変更を行いました(かなり省略しています)

package.json

+    "@emotion/core": "^10.0.27",
+    "@emotion/styled": "^10.0.27",
....
-    "@babel/plugin-proposal-class-properties": "7.1.0",
-    "@babel/plugin-proposal-decorators": "^7.0.0",
-    (@babel/plugin-** が続くため略)
-    "@babel/preset-flow": "^7.0.0",
+    "@babel/preset-typescript": "^7.8.3",
+    "@typescript-eslint/eslint-plugin": "^2.13.0",
+    "@typescript-eslint/parser": "^2.13.0",
+    "babel-plugin-emotion": "^10.0.27",
+    "emotion": "^10.0.27",
-    "flow-bin": "^0.50.0",
+    "jest-emotion": "^10.0.27",
-    "postcss-cssnext": "^2.10.0",
-    "postcss-extend": "^1.0.5",
-     (postcss-* が続くため略)
+    "typescript": "^3.7.4",

.babelrc

-    "@babel/preset-flow"
+     "@babel/preset-typescript",
...
-    "@babel/plugin-transform-flow-strip-types",
-    "@babel/plugin-proposal-function-bind",
-     (@babel/plugin-*** が続くため略)
+    [ "emotion", { "labelFormat": "[filename]--[local]" } ]

.webpack.config.js

-      extensions: ['.webpack.js', '.web.js', '.js', '.jsx', '.json'],
+      extensions: ['.webpack.js', '.web.js', '.js', '.jsx', '.json', '.ts', '.tsx'],
...
-          test: /\.jsx?$/,
+          test: /\.[jt]sx?$/,
...
-          { loader: 'postcss-loader' }

特筆すべきは、Webpack 関連の設定を大きく変えずに済んでいる ことです。

TypeScript 化の方法は2つあり、

  • tsc コマンドなどを使い、TypeScript のツールセットで実現する
  • @babel/preset-typescript を使う

今回は後者にしました。すでに babel を活用している場合は、 @babel/preset-typescript を使うと簡単でおすすめです*3

無理だったら諦めて as any.js

「非破壊TypeScript」にもある通りロジックを変更しないのがとても大事です。ロジックを変更すると、なにかあったときの切り分けが難しくなります。ロジックを変更するぐらいだったら as any でキャストしたり、 .js のままで放置したりしました。

初めて TypeScript 化する場合は、積極的に諦めましょう! Done is better than perfect です。

たとえば、次のようなものにすら as any を使いました。*4

document.querySelector('.some-class-name').setAttribute('hidden', true as any)

.css を .tsx に移行

emotion では、次の2つの書き方が使えます。前者は css として書くことができる反面*5、後者では px を省略したり、パラメータをすっきりした形で使えます。

const Wrapper1 = styled.div<{ height: number }>`
  text-align: center;
  width: 240px;
  height: ${({ height }) => `${height}px`};
  & + & {
    margin-top: 4px;
  }
`
const Wrapper2 = styled.div<{ height: number }>({
  textAlign: 'center',
  width: 240,
  '& + &': {
    marginTop: 4,
  }
}, ({ height }) => ({
  height
}))

今回は「PostCSSから移行する」ということを念頭において、前者の CSS チックな書き方を採用してひたすらコピペしました。個人的には両方とも使いますが、主に後者の object 方式を使うことが多いです。

かかった期間

だいたいの作業としては、年末年始にテレビを見ながら2,3日でかきあげました*6。地道な作業が多いので、ゆっくりお酒を飲んだりバラエティ番組でも見ながらやりきるのがおすすめです。

移行してよかったところ

  • TypeScript 化によって、 Flow では気づけなかった型のミスが出てきた
  • PostCSS では気づけなかった、 使われてない CSS を削除できた
  • 社内メンバーがコントリビュートしやすくなった

1つめに関しては、React の 型指定の誤りや、型の指定が緩すぎるものなどが出てきました。Flow の使い方が悪かった可能性もありますが、TypeScript の方が型定義は厳しめな印象です。例えば、 Set の型パラメータの指定がなくて怒られました。無理なものは諦めた一方、地道な型解決でかなり型安全にできたと思います。

2つめに関しては、typescript-eslint の未使用変数チェックを通るため、使われてない CSS をあぶり出すことができます。仮にうっかり削除しても TypeScript のコンパイルエラーで気付けるので、削除の心理的障壁がありません。

3つめに関しては、TypeScript の方がエコシステムが充実していたり安心感があったりするのか(主観です)、プロジェクトのコアメンバーでなくてもコントリビュートしやすくなったように感じます。実際、FASTALERT 新型コロナ機能のプロジェクトでは、SRE のエンジニアやインターン生のコントリビューションもありました。

speakerdeck.com

まとめ

今回は、信頼性の求められる BtoB SaaS で、Flow を TypeScript に、PostCSS を emotion に移行したときの話でした。差分は数万行に渡ります*7。今回得たベストプラクティスな知見としては、

  • 非破壊 TypeScript を参考にする
  • TypeScript に怒られながら進める
  • ロジックは絶対に変えない、積極的に諦める
  • なにかのついでにやると、地道な作業が多くても問題ない

以上です。後学のため、もしツールなどでスマートに移行する方法があれば教えて下さい!

宣伝

今回ご紹介したとおり、JX 通信社のフロントエンドプロジェクトは、 TypeScript、React、emotion が活用されていたりとモダンな開発環境です。フロントエンド(や Pythonも!) を一緒に書きたいインターン生を積極募集しています。まだ新型コロナも収束していませんので、エリア不問&リモートOKです!

*1:Flow も React も同じ Facebook 由来のため

*2:新型コロナダッシュボード爆速リリースの舞台裏 〜小さく始めて大胆に変えるフロントエンドプロジェクト〜 - JX通信社エンジニアブログ で紹介したような共通ライブラリ化が容易というメリットなどもあります

*3:ただしトランスパイル時の型チェックはされないので、 tsc --noEmit を走らせて型チェックをしてください

*4:ちゃんとやれば any を使わず書き換えられますし、なんならもっと React っぽく移行できそうですが、動いていたものを最優先

*5:プラグインによる色付けもしやすいと思います

*6:他作業もあり、マージされるまでにはこれ以上かかっています

*7:正直に言うと、 eslint の自動フォーマットもあるので盛ってます