141
110

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

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

Last updated at Posted at 2022-07-21

はじめに

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

まず、Typescript のメリットは何か?
この記事などで Typescript の旨味を伝えてくれています。
TypeScript の導入で開発現場はどう変わる?  Sansan の事例に見るメリットとコスト

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

作業実績

  • github 上の作業ブランチ
        01_branch.png

    • Files changedの数が実際より多いことになっていますが、随時 master ブランチから別ブランチの修正を取り込んでいたため多く表示されています。
  • 作業用ブランチと master ブランチの差分から(git diff --stat)

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

環境

  • React(CRA)
  • Javascript
  • npm

方針

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

作業の概要

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

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

※ここまで終わったら

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

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

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

作業内容

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

ts-migrate とは

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 からの移行ガイド」の移行プロセスで言うところの、以下の太字のところを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
  2. ts-migrate をインストールする

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

    • npx ts-migrate-full [移行対象のディレクトリ(srcとか)]
  4. 途中で確認コマンドでてくるので 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-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 も入れておいてます
    {
      "compilerOptions": {
        // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied `any` type.. */
      }
    }
    
    {
      "compilerOptions": {
       // TODO: 一時的にanyを許容するためfalseにしているが、いずれtrueにする
       "noImplicitAny": false /* Enable error reporting for expressions and declarations with an implied `any` type.. */
      }
    }
    
  2. 型推論できないコードに対して any をつけていく

    • 通常パターン 1

      let target;
      

      let target: any;
      
    • 通常パターン 2

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

      export default async function getHoge(from: any, to: any) {...}
      
    • window 関数のパターン

      window.debug();
      

      (window as any).debug();
      
  3. (追加で)引数足りなくて怒られる関数にはとりあえずundefinedを入れる

    • // getAPIClientAsyncは引数を与えることもできる
      const apigClient = await getAPIClientAsync();
      
      // TODO: 後で関数のIFに'?'を付与して optional Chaining にし、undefinedを削除する
      const apigClient = await getAPIClientAsync(undefined);
      

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

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

    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

    let target: any;
    

    interface TargetObject {
      name: string;
      value: number;
    }
    let target: TargetObject;
    
  • 例 2

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

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

対処したエラー一覧

ts-migrate(7016)

  • 内容

    // @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-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-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-expect-error ts-migrate(2339) FIXME: Property 'gtag' does not exist on type 'Global'.
    global.gtag = () => {};
    
  • 原因
    window領域で認識されていないプロパティを使用している(カスタムプロパティの使用)

  • 対処

    @typesディレクトリをルートに作り、window.d.tsを作成する
    02_window_d_ts.png

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

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

ts-migrate(2571)

  • 内容

     this.GetHogeList(hoge).then((hoges) => {
     // @ts-expect-error ts-migrate(2571) FIXME: Object is of type 'unknown'.
     if (hoges.length > 0) {
       ...
     }
    }).catch((e) => {
      ...
    }
    
  • 原因
    型推論ができない

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

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

ts-migrate(2362)

  • 内容

    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型で計算をしようとしている

  • 対処

ts-migrate(2769)

  • 内容

    • case1

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

      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[]を指定してあげる

      const imgArray: any[] = Object.assign([], typeList);
      imgArray.push(customType);
      
    • case2
      thisを削除

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

ts-migrate(2345)

  • 内容

    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()
    );
    
  • 原因
    引数の型が合っていない

  • 対処

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

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

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

service-worker で isolated ts エラー

ts-migrate(2339) 'hot' NodeModule

  • 内容

    // 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));
    }
    
  • 対処

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

スプレッド構文でエラー

  • 内容

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

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

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

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

まとめ

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

参考

JavaScriptからの移行ガイド
JavaScriptからTypeScriptへ徐々に移行していくには?
JSからTSへの移行ツール、ts-migrateを試してみた

141
110
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
141
110

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?