Sansan Tech Blog

Sansanのものづくりを支えるメンバーの技術やデザイン、プロダクトマネジメントの情報を発信

TypeScript を導入して 1 年が経って感じた良かったこと・困ったこと

こんにちは。Eight でエンジニアをしている鳥山(@pvcresin)です。
最近は、デザインとエンジニアリングの距離をもっと近くにできないかということばかり考えています。 なかなか難しい課題で、すぐには答えが出そうにありません。
さて今回は、以前書いた記事「Eight、TypeScript はじめました」の続編となります。

buildersbox.corp-sansan.com

上記の記事では、TypeScript(TS)初心者だった私が Eight の Web フロントエンドに TS を導入するまでの話をしました。 本記事では、TS 導入から約 1 年が経ってみて感じた良かったこと・困ったことについてお話ししたいと思います。 TS 導入を考えている、あるいは移行中の方の参考になればと思います。

おらさい

まずは、TS 導入時の Eight の Web フロントエンドについてのおさらいをします。
Eight の Web フロントエンド は 2012 年から存在する、歴史ある大規模アプリケーションです。 数年前の JavaScript(JS)のコードが現役で動いていることも珍しくなく、コードリーディングに時間がかかるという問題がありました。 また、一部ユニットテストが足りない部分もあり、慎重に実装していく必要がありました。
そこで TS の持つメリットである、

  1. 実行する前にコードのエラーチェックができる(テストに近い役割)
  2. 既存のコードのドキュメント代わりになり、読解を助ける
  3. より賢いコード補完が得られる

の 3 つを享受するため、Eight に導入したのが 2020 年 5 月のことでした。

TS 導入直後の動き

TS を導入したからといって、Eight 全体が次の日から TS を書かけるようにはなりません。 新規のコードを TS で書いてもらうためには、コンポーネントや通信部分など、ある程度参考にできるコードのパターンが必要だと考えました。 そこで、まずは私が率先して新規のコードは TS で書き、既存の JS ファイルは空いた時間で少しづつ型を付与しながら TS に移行するようにしました。 1
ちなみに TS への移行には、ts-expect-error や any を使ってすべての JS ファイルを最初に TS ファイルに変換し、後から正確な型に直していくという方法もあります。 Eight では「TS ファイル = 正確な型を付与した後のファイル」という認識で揃えたかったため、1 ファイルずつ移行する方法をとっています。
1 ヶ月もすれば、おおよそのフロントエンドのコードのパターンが網羅できたため、 フロントエンドを主に担当するエンジニアと一緒に、他のチームでも新規のコードを TS で書くことを推進していきました。
結果としてフロントエンドのファイル数における TS 率は、導入当時(2020 年 5 月)の 0% から、現在(2021 年 6 月)54% まで上昇しています。 まだ半分とも、もう半分とも捉えられますが、約 1 年で歴史ある大規模アプリケーションの過半数のファイルを TS にできたのは、 今後の Eight にとって大きな成果です。

良かったこと

では、本題です。 まずは TS を導入して、良かったことについて。

ドキュメントとしての役割

型はドキュメントとしても有用です。 自分のチームが過去に書いたコードであっても、詳細な仕様を忘れているということはよくあります。 その際、正確な型がわかることで実装から当時の記憶をたどることが容易になりました。 また、他のチームが書いたコードに変更を加える際は、よりドキュメントとしての効果を発揮してくれました。 とあるメンバーからは、 「本来は事前にたくさんの調査を行わないと作業ができないような場面でも、前に作業したチームによってしっかりと型が当てられていたことで、 安心してすぐに作業に入ることができた」という声をいただきました✌ TS を推進することでしっかりと開発を加速させることができていると感じました。

新規追加ファイルは(ほぼ)すべて TS に

現在、 新規追加ファイルはほぼすべて TS で書かれています。 「ほぼ」としているのは、すべての Pull Request に目を通せているわけではないので、断定はできないためです。 導入当初は、いきなり TS で書いていくことは難しいだろうから、新規コードの半分以上が TS で書かれるようになるとよいなと考えていたのですが、 Eight のエンジニアを舐めていたなと今では反省しています 🙇
フロントエンドにおいて、これは非常に重要な意味を持ちます。 フロントエンドは機能的変更だけでなくデザイン的な観点での変更も行われるため、ファイルの追加・削除が激しい分野です。 つまり、新しいコードを TS で書くことを徹底していると TS 率が高まり、古いコード(主に JS のはず)の削除も行われるため TS 率がさらに高まります。 ここで余力があれば、空き時間で古いコードを JS から移行して、TS 率は爆上がりです。 この点から、新規ファイルを TS で書くことはこれからも続けていきたいです。

型を使いこなせるメンバーの増加

少しずつ TS に慣れ、型に対する深い理解を持つメンバーが増えてきました。 導入して少し経った頃、社内では型を組み合せて独自のユーティリティ型を作る「型パズル」と呼ばれる取り組みに注目が集まりました。 そこで Type Challenges という型パズルの問題集を解く社内勉強会を立ち上げました。 現在も週 1 でお昼にオンライン上で集まっては型パズルを解いており、今では型の再起処理程度は朝飯前のメンバーも増えてきています。
型パズルが何の役に立つのか疑問に思う方もいるかもしれません。 プログラミングでは変数などを用いて同じ部分を共通化してメンテナンス性を高めますが、型でも同じことができます。 うまく組み合せると一部の型の変更だけで、それに関連する複数の型を追従させることができます。 型パズルは、この「型を組み合わせる力」を養うものと言えます。 また、型の組み合せを理解できることで、ライブラリの型定義のコードリーティングを助けます。 ライブラリを使っていて型エラーが出てしまった場合に、なぜそのエラーが起こるのかを深堀ることで適切に対処することができます。

技術スタックのアップデート

当然のことですが、Eight の技術スタックに言語として TS を追加することができました。 モダンな Web フロントエンド開発では TS はほぼ必須になっており、開発者体験の観点で他社とようやく肩を並べることができました。 また、Web エンジニア全体のスキルという意味でも TS は重要な要素となってきており、Eight 全体を一段引き上げられたと思います。 さらに、採用という文脈でも 1 つのアピールポイントを作れたと考えています。

困ったこと

次に困ったことです。 困ったことと、それにどう対処したかについて書きます。

学習コストの高さ

これは導入前からわかっていたことですが、TS の学習コストは決して低くありません。 プリミティブ型やその配列なら問題ないですが、複雑なクラスや関数に正しく型を付けていくのは難しいです。 TS を理解するためには JS の知識も必要になるため、そもそも JS に慣れていないエンジニアには余計にハードルが高く感じられると思います。 導入してから、各所でどのようにコードを書けばよいかという議論が湧き起こりました。

これに対してはハンズオンを行うことも必要ですが、最終的には日々のコードレビューやモブプロ、社内勉強会による知見共有で解決するしかないと思っています。 私も導入当初は複数のチームのコードをレビューし、積極的にどのような理由でどういったコードを書けばよいかを共有していきました。
例えば any, unknown, ts-expect-error の使い分けなどは、 指針はあれど場所によって最適なものが変わることもあるので、その都度コメントしました。 また、間違った型は型がないことよりも混乱を引き起こすため、 どうしてもわからない場合は理由を明記した上で型エラーを潰してもよいという方針をとりました。 型についての知見が溜まった頃に戻ってくるとあっさり正確な型が当てられることも少なくありません。
最初の数ヶ月こそレビューコストは増大しましたが、ある程度パターンを掴んでくると、私が入る必要はなくなっていきました。 先に述べたとおり TS の学習コストは決して低くないため、最初にすべてを理解してもらうのは難しいです。 大切なことは、作業前より作業後の方が少しでも安全性という面で前進していることだと思います。

移行する際に想定した型が当たらない

JS から移行する際に、想定した型が当たらない場合があります。 これは複数の場合に対応できるように処理が書かれた関数でよくあるパターンです。 例えば、string なら 2 回繰り返し、number なら 2 倍にして返す関数 double を定義した JS のコードがあるとします。

// string なら 2 回繰り返し、number なら 2 倍にして返す
function double(a) {
  return a + a;
}

const result1 = double("1"); // result1 = '11'
const result2 = double(1);   // result2 = 2

この JS のコードは(良いコードかどうかは置いておいて)問題なく動作します。 そこで引数 a に型を付けると

function double(a: string | number) {
  return a + a; // Operator '+' cannot be applied to types 'string | number' and 'string | number'.
}

string | numberstring | number+ でつなげているため、型エラーが出てしまいました。 では、if で場合分けして string 同士、number 同士でのみ + でつなげてみます。

function double(a: string | number) {
  if (typeof a === "string") return a + a;
  else return a + a;
}

const result1 = double("1"); // result1 の型は string | number
const result2 = double(1);   // result2 の型は string | number

すると今度は型エラーがでません。 これで TS としては問題ないコードになりました。 しかし、result1 と result2 は共に string | number になってしまいました。 実際には result1 は'11'なので string になってほしいですし、result2 は 2 なので number になってほしいです 🥺
そこで double 関数を string 用と number 用の 2 つに分けてみます。

// 文字列の連結
function doubleString(a: string) {
  return `${a}${a}`;
}

// 数値の加算
function doubleNumber(a: number) {
  return a * 2;
}

const result1 = doubleString("1"); // result1 の型は string
const result2 = doubleNumber(1);   // result2 の型は number

これで result1 は string に、result2 は number になりました。
ちなみに、doubleString の方は Generics を使って

function doubleString<T extends string>(a: T): `${T}${T}` {
  return `${a}${a}`;
}

const result1 = doubleString("1"); // result1 の型は '11'

と書けば、result1 の型を string からさらに '11' まで絞り込むことができます。

上記の例では 1 つの関数で複数の場合に対応していたものを 2 つの関数に分けることでより狭い型を各変数に割り当てることができました。 実際には例よりもっと複雑だと思いますが、大切なことは細かく関数や処理を分けたことです。 これは狭い型が当てられるのと同時に、1 つの関数で様々なことを行っていないので実装が読み解きやすくなります。 ぜひ移行時には、求める型は何かを意識してみてください。 また、TS への移行(型の付与)と実装の変更(リファクタリング)を同時に行うとバグを生みやすいので注意しましょう。 既存のコードの移行時に求める型が当たらなかった場合は、挙動を変えずに一旦 TS 化した後に、別の Pull Request でリファクタリングを行うことをおすすめします。

ライブラリの型定義が悪く、想定した型が当たらない

TS では依存ライブラリも重要です。 正しい型定義がないライブラリを使用している場合、普通に使っているだけでもコードに間違った型が当たることになります。 実際、移行時にいくつかの古いライブラリで型定義が見つからなかったり、型定義はあるもののあまり正確ではないということがありました。

対処法としては、

  1. 自前で型定義を書く
  2. 正しい型定義がある代替ライブラリに乗り換える
  3. 当該ライブラリを使わないようにする
  4. どうしようもないので型エラーを潰す

のいずれかになると思います。 ライブラリ側の型定義が間違えている時は修正する Pull Request を送ることもできますが、そういったライブラリはそもそもメンテナンスがあまりされていない場合が多く、マージされない可能性が高そうでした 😇 新規でライブラリを入れる場合には型定義もチェックしましょう。

まとめ

ここまで、TS を導入してから 1 年経って感じた良かったこと・困ったことについてお話しました。
もともと、始めから完璧な TS を書けるとは思っていませんでしたが、この 1 年で Eight の TS 力をかなり高めることができました。 明らかにすべて JS で書いていた頃より安全性は上がっている実感があり、開発速度も変わらないレベルになってきています。 さらに続けていけば、TS の方が速くなって行くと考えています。 また、多少困ったこともありましたが、大規模アプリケーションの開発を安全に続けていくために必要な痛みと捉えています。
今後は、引き続き TS 率を高めつつ、実装に即して型をできる限り狭めて安全性を高めていきたいと思います 💪


  1. JS から TS への移行のことを、その作業の性質から「お型付け(おかたづけ)」と呼んでいます

© Sansan, Inc.