JavascriptからTypescriptへ1人で5万行修正し、移行した話

# はじめに

## なぜ Javascript から Typescript に置き換えたのか？

まず、Typescript のメリットは何か？
この記事などで Typescript の旨味を伝えてくれています。
[TypeScript の導入で開発現場はどう変わる？　 Sansan の事例に見るメリットとコスト](https://codezine.jp/article/detail/14849)

自分がフロントチームとしてアサインした時、開発している Web アプリケーションは運用開始してから 4 年程経っていて、人も入れ替わりの時期でした。
4 年も経っているので仕方がないことなのですが、機能を担当した人しかわからない部分や、ドキュメントに記載されていないような暗黙知みたいなところが少しづつ出てきているような感じでした。
これらは言わば「属人化」という、人に依存するリスクだと思います。
自分が新規参画者側ということもあったため、このアプリケーションの属人化をなるべく排除していきたいというモチベーションがありました。
そこで、まずはコードを静的型付け言語にし、型を強制し暗黙知を減らそうということで Javascript から Typescript への移行の活動を始めていきました。
React アプリケーションで自分は作業していますが、Javascript->Typescript への移行作業自体はフレームワークに依存しないので、他のフレームワークでも応用できると思います。

# 作業実績

- github 上の作業ブランチ
  　　　　![01_branch.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/60bc3e47-945b-bce9-316a-cead1b57f0d1.png)


  - `Files changed`の数が実際より多いことになっていますが、随時 master ブランチから別ブランチの修正を取り込んでいたため多く表示されています。

- 作業用ブランチと master ブランチの差分から(`git diff --stat`)

  | 項目               | 実績           |
  | ------------------ | -------------- |
  | 修正ファイル数     | 887 ファイル   |
  | 修正コード行数     | 51477 ステップ |
  | 対応にかかった期間 | 約 2 ヶ月半    |
  | 対応人数           | 1 人           |

# 環境

- React(CRA)
- Javascript
- npm

# 方針

- 当たり前の前提として、既に動いている機能は壊さない
- Javascript -> Typescript へ最短で置き換えて、無事起動するところまでを第一の目標とする
- any 型を許容する（後の改修や機能追加で型を順次付与していく想定）

# 作業の概要

## コンパイルエラーを解消し、最低限動作するまで

1. ts-migrate を使って全体的に Typescript へ移行する
1. ts-migrate で出力された指摘を手作業で修正し、指摘コメントを削除していく
1. 依存ライブラリに必要な`@types`を install していく
1. 型が無くてエラーになっているところに対して any で型を付与していく

### ※ここまで終わったら

- この時点で master にマージしても問題はない
- マージする場合は既存機能が問題なく動作するかしっかり確認する！
- 一旦マージしてチーム全員で型品質を上げることに取り組んでいくか、ある程度品質が上がるまで別ブランチで作業するかは話し合って決める

## 最低限動作することを確認した上で、Typescript としての品質を上げていく

1. API との IF の型を定義して適用する
1. any 型を付与したところを順次型付けしていく

# 作業内容

## ts-migrate を使って全体的に Typescript へ移行する

### ts-migrate とは

[ts-migrate](https://www.npmjs.com/package/ts-migrate)は Javascript から Typescript への移行をサポートしてくれるツールです。
以下のステップで構成されます。

- `[Step 1 of 4] Initializing ts-config for the "src"...`
  - tsconfig.json を作成します
- `[Step 2 of 4] Renaming files from JS/JSX to TS/TSX and updating project.json\...`
  - プロジェクト配下の`js/jsx`拡張子のファイルを`ts/tsx`拡張子ファイルに変換します
- `[Step 3 of 4] Fixing TypeScript errors...`
  - ts-migrate の方で対応できる Typescript エラーを自動で解消します
- `[Step 4 of 4] Checking for TS compilation errors (there shouldn't be any).`
  - ts-migrate の方で対応できなかった Typescript エラーに対して`@ts-expect-error`コメントを付与して開発者に意図的にエラーとして気づかせるようにします

[Typescript Deep Dive の「JavaScript からの移行ガイド」の移行プロセス](https://typescript-jp.gitbook.io/deep-dive/type-system/migrating)で言うところの、以下の太字のところを`ts-migrate`は自動で対応してくれます。

> - **tsconfig.json を追加する**
> - **ソースコードの拡張子を .js から.ts に変更する。any 型を使ってエラーを抑制する**
> - 新しいコードは TypeScript で記述し、できるだけ any を使わないようにする
> - 古いコードに戻り、型アノテーションを追加し、バグを修正する
> - サードパーティ製 JavaScript コードの型定義を使用する

このツールを利用することで、移行作業にかかる時間が大幅に節約できます。（単純作業で目が死ぬこともない:skull:）

### 手順

1. ts-migrate を実行する前の下準備をする

   - 移行用のブランチを作る・・・`git checkout -b ts-migrate`
   - 最新の状態にする・・・`git pull origin master`
   - `node_modules`を最新の状態にしておく・・・`npm install`

1. ts-migrate をインストールする

   - `npm install --save-dev ts-migrate`
   - プロジェクト内に移行ツール入れたくなければ global でも良かった気がする

1. ts-migrate を実行する

   - `npx ts-migrate-full [移行対象のディレクトリ(srcとか)]`

1. 途中で確認コマンドでてくるので Enter で進めつつ ts-migrate が終わるのを待つ
   - 完了メッセージがでれば OK
     ```
     ---
     All done!
     ```

## ts-migrate で出力された指摘を手作業で修正し、指摘コメントを削除していく

ts-migrate で自動で直せなかった箇所については、ts-migrate がコメントをつけてくれています。

```
// @ts-expect-error ts-migrate(XXXX) FIXME:
```

指摘されたコメント部分を対応すると、コメント自体が赤くなるので、コメントを消し、その指摘箇所については対処完了となります。

自分が対応した指摘については後半でまとめています。

## 依存ライブラリに必要な`@types`を install していく

```
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module
```

このエラー系は`@types`ライブラリが足りないので追加でインストールしていきます

例えば以下のような指摘です

```ts
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'reac... Remove this comment to see the full error message
import React from "react";
```

`npm install -D @types/react`で解決します。

## 型が無くてエラーになっているところに対して any で型を付与していく

1. `tsconfig.json`に any を許容する設定を入れる

   - `noImplicitAny`がコメントアウトされてるので外す。一応 TODO も入れておいてます

   ```json
   {
     "compilerOptions": {
       // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
     }
   }
   ```

   ```json
   {
     "compilerOptions": {
      // TODO: 一時的にanyを許容するためfalseにしているが、いずれtrueにする
      "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */
     }
   }
   ```

1. 型推論できないコードに対して any をつけていく

   - 通常パターン 1

     ```ts
     let target;
     ```

     ↓

     ```ts
     let target: any;
     ```

   - 通常パターン 2

     ```ts
     export default async function getHoge(from, to) {...}
     ```

     ↓

     ```ts
     export default async function getHoge(from: any, to: any) {...}
     ```

   - window 関数のパターン
     ```ts
     window.debug();
     ```
     ↓
     ```ts
     (window as any).debug();
     ```

1. (追加で)引数足りなくて怒られる関数にはとりあえず`undefined`を入れる
   - 例
     ```ts
     // getAPIClientAsyncは引数を与えることもできる
     const apigClient = await getAPIClientAsync();
     ```
     ↓
     ```ts
     // TODO: 後で関数のIFに'?'を付与して optional Chaining にし、undefinedを削除する
     const apigClient = await getAPIClientAsync(undefined);
     ```

## API との IF の型を定義して適用する

- 例

  ```ts
  export default async function getBackendInfo({startDate,endDate}: any){
    const apiResponseData = await ...
    ...
    ...
    return apiResponseData;
  }
  ```

  ↓

  ```ts
  interface GetBackendInfoReq {
    startDate:string;
    endDate?:string;
  }
  interface GetBackendInfoRes {
    data: {
      name: string;
      value: number;
    }
  }
  export default async function getBackendInfo({startDate,endDate}: GetBackendInfoReq): Promise<GetBackendInfoRes>{
    const apiResponseData = await ...
    ...
    ...
    return apiResponseData;
  }

  ```

## any 型を付与したところを順次型付けしていく

- 例 1

  ```ts
  let target: any;
  ```

  ↓

  ```ts
  interface TargetObject {
    name: string;
    value: number;
  }
  let target: TargetObject;
  ```

- 例 2

  ```ts
  export default async function getHoge(from: any, to: any) {...}
  ```

  ↓

  ```ts
  export default async function getHoge(from: string, to: string) {...}
  ```

# 対処したエラー一覧

## `ts-migrate(7016)`

- 内容
  ```ts
  // @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'react... Remove this comment to see the full error message
  import React from "react";
  ```
- 原因
  Typescript 用モジュールがなくて読み込めていない
- 対処

  `npm install -D @types/react`

## `ts-migrate(2339)`

- 内容
  ```ts
  // @ts-expect-error ts-migrate(2339) FIXME: Property 'trace' does not exist on type 'Window & ... Remove this comment to see the full error message
  window.trace("canceled");
  ```
  ```ts
  // @ts-expect-error ts-migrate(2339) FIXME: Property 'clients' does not exist on type 'Window ... Remove this comment to see the full error message
  self.clients.matchAll();
  ```
  ```ts
  // @ts-expect-error ts-migrate(2339) FIXME: Property 'gtag' does not exist on type 'Global'.
  global.gtag = () => {};
  ```
- 原因
  `window`領域で認識されていないプロパティを使用している(カスタムプロパティの使用)
- 対処

  - 参考:[TypeScript の `window`に新しいプロパティを明示的に設定するにはどうすればいいですか？](https://www.web-dev-qa-db-ja.com/ja/typescript/typescript%E3%81%AE-%60window%60%E3%81%AB%E6%96%B0%E3%81%97%E3%81%84%E3%83%97%E3%83%AD%E3%83%91%E3%83%86%E3%82%A3%E3%82%92%E6%98%8E%E7%A4%BA%E7%9A%84%E3%81%AB%E8%A8%AD%E5%AE%9A%E3%81%99%E3%82%8B%E3%81%AB%E3%81%AF%E3%81%A9%E3%81%86%E3%81%99%E3%82%8C%E3%81%B0%E3%81%84%E3%81%84%E3%81%A7%E3%81%99%E3%81%8B%EF%BC%9F/1069391322/amp/)

  `@types`ディレクトリをルートに作り、`window.d.ts`を作成する
    ![02_window_d_ts.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2641313/cda75964-a2e3-d4f3-5892-9bc5deedf2e5.png)


  - `window.d.ts`にカスタムプロパティを設定する(とりあえず `any` でも OK)

    ```ts
    interface Window {
      loginUser: { [key: string]: any };
      trace: any;
      clients: { [key: string]: any };
      gtag: any;
    }
    let window: Window;
    ```

## `ts-migrate(2571)`

- 内容

  ```ts
   this.GetHogeList(hoge).then((hoges) => {
   // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
   if (hoges.length > 0) {
     ...
   }
  }).catch((e) => {
    ...
  }
  ```

- 原因
  型推論ができない

- 対処
  型チェック、または any を指定

  ```ts
   this.GetHogeList(hoge).then((hoges: any) => {
   if (hoges.length > 0) {
     ...
   }
  }).catch((e) => {
    ...
  }
  ```

## `ts-migrate(2362)`

- 内容

  ```ts
  alertAfter = get(data[ALERT_NAME[index]], 'after') || 0;
  alertBefore = get(data[ALERT_NAME[index]], 'before') || 0;

  // @ts-expect-error ts-migrate(2362) FIXME: The left-hand side of an arithmetic operation must... Remove this comment to see the full error message
  if (alertAfter - alertBefore < 0) {
    ...
  }
  ```

- 原因
  `string`型で計算をしようとしている

- 対処

  - [The left-hand side of an arithmetic operation must be of type 'any', 'number' or an enum type](https://stackoverflow.com/questions/36560806/the-left-hand-side-of-an-arithmetic-operation-must-be-of-type-any-number-or)

  - `parseInt`などで`number`型に変換する

    ```ts
    alertAfter = parseInt(get(data[ALERT_NAME[index]], 'after')) || 0;
    alertBefore = parseInt(get(data[ALERT_NAME[index]], 'before')) || 0;

    if (alertAfter - alertBefore < 0) {
      ...
    }
    ```

## `ts-migrate(2769)`

- 内容

  - case1

    ```ts
    const imgArray = Object.assign([], typeList);
    // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
    imgArray.push(customType);
    ```

  - case2
    ```ts
    video.addEventListener(
      "playing",
      () => {
        this.setState({ videoState: PLAYING });
        // @ts-expect-error ts-migrate(2769) FIXME: No overload matches this call.
      },
      this
    );
    ```

- 原因

  - case1
    `typeList`オブジェクトが暗黙的に型となり、`push`しようとしている`customType`オブジェクトの変数が`typeList`と比べて不足しているものがあった
  - case2
    `addEventListener`の引数として当てはまらない

- 対処

  - case1
    `any[]`を指定してあげる

    ```ts
    const imgArray: any[] = Object.assign([], typeList);
    imgArray.push(customType);
    ```

  - case2
    `this`を削除

    ```ts
    video.addEventListener("playing", () => {
      this.setState({ videoState: PLAYING });
    });
    ```

## `ts-migrate(2345)`

- 内容

  ```ts
  window.sessionStorage.setItem(
    "exp",
    // @ts-expect-error ts-migrate(2345) FIXME: Argument of type 'number' is not assignable to par... Remove this comment to see the full error message
    moment(credentials.expireTime).unix()
  );
  ```

- 原因
  引数の型が合っていない

- 対処

  - 型が合うように変換する

    ```ts
    window.sessionStorage.setItem(
      "exp",
      String(moment(credentials.expireTime).unix())
    );
    ```

## styled-component の props で typescript に引っかかる

- 内容

  ```ts
  export const Div_Container = styled.div`
    background: ${(props: any) => (props.color ? props.color : "#ffffff")};
  `;
  ```

- 原因
  styled-component 内での変数が typescript で認識されていないので怒られる

- 対処

  - [TypeScript で styled-components を使う時 props で style の制御ができなくて困った時の解決法](https://qiita.com/koralle/items/bf817476725c7a49a04c)

  - interface を定義する

    ```ts
    interface ContainerProps {
      color?: string;
    }
    export const Div_Container = styled.div<ContainerProps>`
      background: ${(props: ContainerProps) =>
        props.color ? props.color : "#ffffff"};
    `;
    ```

## service-worker で isolated ts エラー

- 内容
  ```
  cannot be compiled under ‘--isolatedModules’ because it is considered a global script file. Add an import, export, or an empty ‘export {}’ statement to make it a module.
  ```
- 対処

  - [Why is --isolatedModules error fixed by any import?](https://stackoverflow.com/questions/56577201/why-is-isolatedmodules-error-fixed-by-any-import/56577324)

## ts-migrate(2339) 'hot' NodeModule

- 内容

  ```ts
  // using hot reload
  // @ts-expect-error ts-migrate(2339) FIXME: Property 'hot' does not exist on type 'NodeModule'... Remove this comment to see the full error message
  if (module.hot) {
    // @ts-expect-error ts-migrate(2339) FIXME: Property 'hot' does not exist on type 'NodeModule'... Remove this comment to see the full error message
    module.hot.accept("./reducers", () => store.replaceReducer(reducers));
  }
  ```

- 対処

  - [React: TypeScript の lint で module.hot 周りでエラーが出た際の解決方法](https://qiita.com/Statham/items/695da38da327238e0c34)

  - `npm install -D @types/webpack-env`

## Property 'style' does not exist on type 'Element'.

- 内容

  ```ts
  Element implicitly has an 'any' type because expression of type '"style"' can't be used to index type 'Element'.
  Property 'style' does not exist on type 'Element'.
  ```

- 対処

  - [TypeScript で遭遇したエラー: プロパティ 'style' は型 'Element' に存在しません。](https://www.l08084.com/entry/2018/03/08/193347)

## スプレッド構文でエラー

- 内容

  ```ts
  is not an array type or a string type. Use compiler option '--downlevelIteration' to allow iterating of iterators.
  ```

- 対処

  - [TypeScript スプレッド構文で配列に変換しようとしたらエラーになった](https://chaika.hatenablog.com/entry/2020/08/17/083000)
  - `tsconfig.json`の`downlevelIteration`オプションを有効にする

    ```json
    {
      "compilerOptions": {
        // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
      }
    }
    ```

    ↓

    ```json
    {
      "compilerOptions": {
        "downlevelIteration": true /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
      }
    }
    ```


# Typescript 移行後にチーム内で取り決めたコーディング規約

Typescript への移行が終わったあと、今後も継続的に Typescript としての型品質を上げること、チーム内で型への認識を統一することを目標として以下をコーディング規約に追加しました。

- 新規、改修で手を入れるコードについては any 型の型指定は禁止（既存は暫定的に設定しているので。）
- String,Number 型は使用しない。string,number 型を使う（大文字と小文字の型は違うので注意！）

# まとめ

通常の開発業務も行いながら細々とやっていたので 2 ヶ月もかかってしまいましたが、2,3 人で集中して作業すればもっと早く終わったと思います。
最初のマイグレーション作業以降は一つ一つ`FIXME:`を潰していくパワーゲームなので・・・。
ですが、細々とやっても 1 人で終わらせることができるということが分かったのは良かったと思います。(永遠の作業ではない:innocent:)
この記事の内容だけでは完全にTypescriptへ移行ができたということにはならないと思います。
まずはJavascriptからTypescriptへ、言語として移行ができるということが最初のステップになるかなと思います。
そして実際に導入された上で、日々の機能改善活動の中でTypescriptとしても品質を上げていくようなリファクタリングを行う流れになるかなと。
Typescript へ移行しようとしているエンジニアの参考になればと思います。

# 参考

[JavaScriptからの移行ガイド](https://typescript-jp.gitbook.io/deep-dive/type-system/migrating)
[JavaScriptからTypeScriptへ徐々に移行していくには？](https://rightcode.co.jp/blog/become-engineer/javascript-typescript-migration#JavaScript_TypeScript_ts-migrate)
[JSからTSへの移行ツール、ts-migrateを試してみた](https://qiita.com/Sotq_17/items/56403d3b8137affc1d10)