watanabiの日記

健康法や普段思っていることを、いろいろ書いています。

そこそこ大きいNext.jsのプロジェクトにTyepeScriptを導入した話


最近、そこそこ大きいNext.jsのプロジェクトをJavascriptからTypeScriptに変更する対応をしました。 最近は健康系のエントリーばかり書いてましたが、自分がエンジニアだったことをふと思い出したのと、あまりwebにTypescript系の情報がなかったので、やったことをまとめておきます。

罠とか後悔ポイントもあったので、JSからTSに移行したいと思ってる人には参考になる点があると思います。

基本情報・構成

かなりメジャーなECサイトで、フロントエンドは10数人います。

主な構成

  • React.js
  • Mobx
  • Next.js
  • Jest(罠多め)
  • Storybook

対応内容

やることは大きく4つ

  • ソースコードをjs(jsx)→ts(tsx)に変更
    • ビルドエラーの対応
  • Next.jsのTS対応
  • JestのTS対応
  • StorybookのTS対応 です。

GitHub - deptno/next.js-typescript-starter-kit: Next@7, Styled-jsx, TypeScript, Jest, SEO
これが今回やった設定とかなり似ています。

Next.js

公式の通りに@zeit/next-typescriptを使って対応しました。

next-plugins/packages/next-typescript at master · zeit/next-plugins · GitHub

設定ファイルは基本的に公式の通り追加しました。

@zeit/next-typescriptはbabelを使ってトランスパイルしている模様。 つまり、ビルド時には型チェックが走りません。
#型情報を落としているだけ

また、tscと比べていくつか使えない機能もあります。

Babel7 or TypeScript | ts-jest

ForkTsCheckerWebpackPluginを使えばトランスパイル時に型チェックできますが、使うかはパフォーマンスも面との兼ね合いだと思います。 現状、エディタの"PROBLEMS"のタブにエラーの箇所が出るので、コンパイル時にチャックしないでエディタに任せても良いとは思います。 VS Codeだとtsconfig.jsonの設定通りに"PROBLEMS"が表示されます。
#ただ、どこかのタイミングでコンパイル時の型チェックを入れたい

ハマりポイント

謎の不親切なエラーメッセージがでました。

Module build failed (from ../node_modules/next/dist/build/webpack/loaders/next-babel-loader.js):
Error: .presets[0][1] must be an object, false, or undefined

nextでビルドしたら上記のエラーが出ていて、何が理由なのか分からず.babelrcを少しずつ削っていったら、

{
"preset-env": {
"modules": "commonjs"
}
}

の記載がアウトだった模様。

moduleシステムは基本的にES Modulesに統一しました。 しかしgulpとNext側からも呼ばれるJSファイルもあり(env系とか)、なかなかカオスに。

環境設定は以下のようなpluginで入れて、gulpのビルドスクリプトとnext.js上で動くアプリのコードを厳密に分割したほうが良かったかもしれません。

EnvironmentPlugin

#力尽きたのでそこまではやってません。

ソースコード修正

全てのjs(jsx)をtsとtsxにリネームしました。 git mvを使ってリネーム。 IDEとかでrenameすると、jsファイルを削除してからtsファイルを追加した扱いになるので、 他のブランチで並行して実装してるJSの内容がマージできなくなります。

再帰的にやるにはこんな感じ。

git - gitで複数のファイルを一括で移動またはリネームする方法を教えてください - スタック・オーバーフロー

型チェック

基本コンパイル時には走らずに、エディタにチェックを任せています。
#違反したproblemの欄に警告が出る。

ライブラリの型情報は以下のようにnpmで取得します。
npm install --save @types/react

@types/react - npm

@typesで取得できない場合は、自前で.d.tsファイルを作るしかないです。

型チェックをすると6000くらいエラーが出るので、どのみちコンパイル時にチェックはできません。。。

これからみんなでコツコツとエラーを減らしていくしかないです。。。

コンパイルエラーの修正

基本的にはTSはJSの文法と互換性があるのですが、いくつかエラーになる箇所がありました。
import hoge from 'hoge.js';
みたいに.jsの拡張子をつけちゃってる部分。

あとは、require()を使っている部分をimportに変更。
思ったより普通に動きました。(next/dynamicとかも)

Jest

基本的にはJestの公式の通り進めて、問題があったら都度対処しました。

Jest · TypeScript Deep Dive

ここにきて、Next.jsはbabelでトランスパイルしてるのに、Jestのts-jestはtscでトランスパイルしている微妙な状態に気がつく。 #後戻りできないのでそのまま進めました。

tsの設定は、tsconfig.jest.jsonみたいにして、jest用のものを作りました。

{
"extends": "./tsconfig.json",
"compilerOptions": {
"module": "commonjs",
"jsx": "react",
"esModuleInterop": true,
}
}

ハマりポイント

Jestは結構エラーや使えなくなった機能がでました。

TSになって、モジュールロードの仕組み変わったため、いくつか影響が出ました。 jest.mock('moduleName')でモジュールの関数をすべてmockにするやり方が使えなくなったり、rewireなどモジュールシステムに依存している機能は使えなくなっています。

あと、gulpでも呼ばれるenv系のjsを呼ぶためにtsconfig.jsonの設定が少し大変でした。
#やっぱり時間をかけてでも統一したほうが良かったかもなー

テストコードってリフレクション的なのを使ったり力技になりがちですが、力技系は結構Typescript対応で駄目になってました。

jest.mock

jest.mock('moduleName')は

jest.mock('moduleName',()=>{
return {
hogeFunc:jest.fn()
}
});

のように明示的にモック化する関数などを指定する必要があります。 #公式のFAQでesModuleInteropの設定とかでjest.mock('moduleName')が使えると書いてあった気がしますが、僕の環境はいくら設定を見直しても駄目でした。

rewire

privateな変数にアクセスできるようにするrewireはTypescript対応によって、使用できなくなりました。

GitHub - jhnns/rewire: Easy monkey-patching for node.js unit tests

これはJavaでいうリフレクションでUTをやってるようなものなので、基本的にはテスト対象のコードの実装が良くないのが原因ですし、 あまり使用箇所も無かったのでrewire無しでテストできるようにコード書き直すようにしました。

他に起きたエラー

ReferenceError: React is not defined

import React from 'react';
せずに、jsx構文を使うと起きる

もしかしたらnextでは起きないで、jestだけで起きるかもしれません。 tsconfig.jsonの"jsx"オプションの違いかもしれない

storybook

https://storybook.js.org/configurations/typescript-config/

基本は公式通り設定

制限事項

propTypesの定義が見れなくなった。 TSでproptypes(interface)をstorybookで表示するためのライブラリがあるのですが、コンポーネントによって、表示されたりされなかったりで少し微妙だったので、外しました。
◯ extends React.component
X extends component(表示されない)
みたいに書き方を固定しないと駄目っぽかったり、Functional componentだと表示できないとか色々問題がありました。

設定ファイル

Storybook用の設定ファイルを作りました。
.babelrc(Storybook用)
webpack.config.js(Storybook用)
tsconfig.storybok.json

基本jestと同様tscコンパイル(型チェックなしにしました)

今回の対応で残念だった点

トランスパイル方法がツールによって違う

公式の通り進めたら、Next.js(babel),Jest(tsc),Storybook(tsc)となった。。。

しかも、モジュールロードの設定も異なる tsconfig.jsonの"module","jsx","esModuleInterop"が違います。
#これは、JSのモジュールシステムがそもそもカオスなのでしょうがない

エラーの量が多すぎてコンパイル時に型チェックできない。

型チェックをすると6000くらいエラーになるので、コンパイル時のチェックを外すしかない状態に。
これではリファクタリングした時に、参照先の他のファイルでデグレってもエラーに埋もれます。
みんなでコツコツ型をつけていくしか無い。。。

もちろん、strictはfalseだし、設定は一番甘くしてます。
ただ、strictNullChecksだけは後から変えにくそうなので、trueにしておきました。
#後からnullチェックを有効にしたら、型定義をもう一度書く羽目になるので

良かった点

コード補完が賢くなった!!!
変数のtypoとかが直ぐに分かる!!!
コードジャンプがちゃんと効くようになった。
#前までうまく効いてなかった
moduleをES modulesに統一したのでtree shakingが効いてビルド後の容量が3割くらい減った。

静的型付き言語をやってきた人には当然の環境がやっと手に入りました。

これから、型情報やinterfaceを書いていけばもっと便利になりそう!

まとめ

動いているJSのコードをTSに変えるのはかなりの労力がいる作業でした。
2,3週間くらいかかりました。 1週目でconf周りが終わって、残りはJestのエラーの箇所をコツコツ修正する感じでした。 環境起因の深めのエラーが多いので原因の調査に凄い時間がかかりました。。。

  • 公式通りにそれぞれTSを入れると、babelとtscが混在しちゃう罠がある
  • module周りの挙動が変わるのが大変
    commonJSとES modulesが混在してたら少し大変
    Jestの対応はかなり大変
  • 型定義が無いのでエラーであふれる  →コツコツ型定義を入れるしか無い

ただ、TSを入れる価値はあると思います。
VSCodeが凄い賢くなりました。(ニッコリ)