開発に使ってみた所感
良かった点
開発効率の向上を実感できた
TypeScriptで書かれたコードに⼿を加える場合、JavaScriptの場合と⽐べて格段に開発効率が上がっているのを実感しました。⼀度これを体験すると、新規のコードはTypeScriptで書いていきたいという気持ちが強くなります。また、通常の開発に追われてなかなか⼿につかない既存コードの移⾏も、今後の開発効率が向上することを確信できれば、積極的に⾏っていきたいという意識が⽣まれます。なにより、型によってコードの挙動を把握しやすいため、安⼼して開発を⾏うことができるようになりました。
移⾏はコツを掴めば進めやすい
既存コードのTypeScript移⾏は難しそうだと思われますが、難易度の低い部分から進めて慣らしていけばそれほど難しくありません。そもそも移⾏⾃体は、拡張⼦を変更して型エラーを解消していくだけの単純作業です。移⾏を難しくするのは、対象としているファイルがさまざまなファイルに依存していたり、複雑なライブラリを組み合わせていたりといった場合がほとんどです。そこで、依存ファイルや依存ライブラリが少なく、短いコードから始めると進めやすいと感じました。
また、いきなり完璧な型を当てるのではなく、わからなかったら型エラーを潰しても良いという⽅針に従うことが重要です。部分的に型エラーを潰しても、それ以外の部分は問題なく型の恩恵が受けられる上、今後加える変更も型を意識して進めることになります。これは⼀部の型エラーで移⾏を諦め、JavaScriptファイルのままであるより、良い状況と⾔えるでしょう。そして、TypeScriptの型についての知⾒が溜まった頃に、潰した型エラーを解消できないか再度検討すれば良いと思います。
そして、移⾏を妨げるもう⼀つの要因は、型を付与したことで既存コードの潜在的なバグに気付いてしまい、ロジックを直してしまうことです。直しだすとキリがないことに加えて、コードに新たなバグを埋め込んでしまう可能性もあります。移⾏時には既存のロジックの変更を⾏わず、型エラーを解消するということを徹底します。⼀度移⾏してしまえば、その型を頼りにリファクタリングを⾏うことも可能です。
困ったこと
設定が複雑になる
導⼊時に最も頭を悩ませたのは、ビルドやツール周りの設定でした。⼀気にすべてのファイルをTypeScriptに移⾏すれば、もう少し設定をシンプルにできたかもしれないですが、こればかりは仕⽅ありません。JavaScript向けの設定を壊さないよう、⼀つひとつ丁寧に必要となるツールや設定項目について調べていきました。とはいえ、⼀度設定してしまえば頻繁に変えるものでもないので、⼤きく問題にはならないと思います。
学習コストの⾼さ
TypeScriptの学習コストは決して低くありません。プリミティブ型やその配列なら問題なくとも、複雑なクラスや関数に正しく型を付けていくのは簡単ではありません。TypeScriptを理解するためにはJavaScriptの知識も必要になるため、そもそもJavaScriptに慣れていないエンジニアには余計にハードルが⾼いように感じられると思います。実際、導⼊してから各所で、どのようにコードを書けば良いかという議論が巻き起こりました。また、導⼊直後はJavaScriptによる開発と⽐較して開発の速度は落ちました。
これに対してはハンズオンを⾏うことも必要ですが、最終的には⽇々のコードレビューやモブプロ、社内勉強会による知⾒共有で解決するしかないと思っています。導⼊当初は、⽐較的⾃信のあるメンバーが複数のチームのコードをレビューし、積極的にどのような理由でどういったコードを書けば良いかを共有していきました。最初の数か⽉こそレビューコストは増⼤しましたが、次第に必要なくなり、開発速度に関してもJavaScriptによる開発と同程度になっていきました。
JavaScript自体に慣れていないメンバーにはJavaScriptの復習の章があるサバイバルTypeScriptを参考にするのがおすすめです。より深くJavaScriptの仕様を理解したい場合はJavaScript Primerも合わせて読むと良いでしょう。
また、間違った型アノテーションは型が書かれていないことよりも混乱を招くため、どうしてもわからない場合は新規のコードであっても理由を明記した上で型エラーを潰して良いという⽅針をとりました。これも移⾏時と同じく、知⾒が溜まった頃に再度確認することになります。
移⾏する際に、求める型が得られない
JavaScriptから移⾏する際に、求める型が得られない場合がありました。これは複数の場合に対応できるように処理が書かれた関数でよくあるパターンです。例えば、stringなら2回繰り返し、numberなら2倍にして返す関数を定義したJavaScriptのコードがあるとします。
// stringなら2回繰り返し、numberなら2倍にして返す
function double(a) {
return a + a;
}
const result1 = double("1"); // result1 = '11'
const result2 = double(1); // result2 = 2
このコードは(良いコードかどうかは置いておいて)問題なく動作します。そこで引数aに型を付けると
function double(a: string | number) {
return a + a; // Operator '+' cannot be applied to types 'string | number' and 'string | number'.
}
string | numberとstring | numberを+でつなげている部分で型エラーが出てしまいました。ロジックに手を加えずに移行するにはこのまま型エラーを潰すほうが良いのですが、今回は説明のためにリファクタリングも行ってみます。場合分けをして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
となります今度は型エラーが出ず、TypeScriptとして問題ないコードになりました。
しかし、result1とresult2の型は共にstring | numberになってしまいました。実際にはresult1は'11'なのでstringが望ましく、result2は2なのでnumberが望ましいです。そこで関数を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(a: T): `${T}${T}` { return `${a}${a}`; } const result1 = doubleString("1"); // result1の型は'11'
と書けば、result1の型をstringからさらに'11'まで絞り込むことができます。
このように、単純に移⾏して型エラーを解決するだけでは広い型が当たってしまい、求める型が得られない場合がありました。そういった場合は移⾏後にリファクタリングを⾏い、細かく関数や処理を分けることで、読み解きやすく求める型が得られるコードになりました。
ライブラリの型定義が悪く、想定した型が当てられない
TypeScriptプロジェクトでは依存するライブラリも重要です。TypeScriptへの知⾒が溜まっているにもかかわらず、想定した型が当てられない場合、ほとんどがライブラリの型定義がない、もしくは間違っていたり・anyを多用していたりと品質が低いことが原因です。そのようなライブラリを使⽤している場合、⾃然とコード品質が下がってしまいます。実際、移⾏時にいくつかの古いライブラリで型定義が⾒つからないことや、型定義はあるもののあまり正確ではないということがありました。
対処法としては、
- ⾃前で型定義を書く
- 正しい型定義がある代替ライブラリに乗り換える
- 当該ライブラリを使わないようにする
- どうしようもないので型エラーを潰す
のいずれかになります。ライブラリ側の型定義が間違えているときは修正するPRを送ることもできますが、そもそもメンテナンスがあまりされていない場合が多く、マージされない場合もあります。新規でライブラリを採用する際には型定義もチェックしましょう。
導⼊後の変化
2020年5⽉にEightのフロントエンドにTypeScriptを導⼊してから、1年以上が経過し、さまざまな変化が起きました。
新規ファイルはほぼTypeScriptに
導⼊当初は、いきなりすべての新規ファイルをTypeScriptで書くことは難しく、JavaScriptのコードも混じっていました。しかし現在、新規ファイルはほぼすべてTypeScriptで書かれています。 導⼊時には新規ファイルの半分以上がTypeScriptになればと良いと考えていたので、想定以上でした。この結果はメンバーがJavaScriptに慣れていたことや、Reactと型との相性が良いことに起因すると考えています。新しいコードをTypeScriptで書くことを徹底していると、TypeScriptファイルを消したりJavaScriptに書き直したりしない限り、常にTypeScript率が⾼まっていくことになります。
ドキュメントとしての効果を発揮
新規ファイルがTypeScriptで書かれるようになったことに加え、既存のJavaScriptファイルの移⾏も隙間時間に粛々と進めています。リポジトリ内のTypeScriptファイルの割合は現在、約6割にまで上昇しています。TypeScriptのコードが増えるにつれてドキュメントとしての効果を発揮しており、コードが劇的に読みやすくなりました。これにより、他チームがメインで担当する部分の実装にも⼿を加えやすくなり、新メンバーのオンボーディングも容易になりました。
型を使いこなせるメンバーの増加
少しずつTypeScriptに慣れ、型に対する深い理解を持つメンバーが増えてきました。導⼊して少し経った頃、社内では型を組み合わせて独⾃のユーティリティ型を作る「型パズル」と呼ばれる取り組みに注⽬が集まりました。そこでTypeChallengesという型パズルの問題集を解く社内勉強会を⽴ち上げました。現在も週に1回、お昼にオンライン上で集まっては型パズルを解いており、今では型の再起処理程度なら朝飯前のメンバーも増えてきています。
型パズルが何の役に⽴つのか疑問に思う⽅もいるかもしれません。プログラミングでは変数などを⽤いて同じ部分を共通化してメンテナンス性を⾼めますが、型でも同じことができます。うまく組み合わせると⼀部の型の変更だけで、それに関連する複数の型を追従させることができます。型パズルは、この「型を組み合わせる⼒」を養うものと⾔えます。また、型の組み合わせを理解できることで、ライブラリの型定義のコードリーティングを助けます。ライブラリを使っていて型エラーが出てしまった場合に、なぜそのエラーが起こるのかを深堀ることで適切に対処することができます。
技術スタックのアップデート
当然のことですが、Eightの技術スタックに⾔語としてTypeScriptを追加することができました。 モダンなWebフロントエンド開発では、ほぼ必須の要素になってきており、開発者体験の観点で他社とようやく肩を並べることができました。また、Webエンジニア全体のスキルという意味でもTypeScriptは重要な要素となってきており、組織全体を⼀段引き上げられたと思います。さらに、採⽤という⽂脈でも1つのアピールポイントを作れたと考えています。
まとめ
JavaScriptによる開発への課題感からTypeScriptの採⽤を決定し、導⼊を⾏いました。 TypeScriptによる開発経験がないメンバーも多かったですが、社内勉強会や実際の業務を通して、定着させることができました。TypeScriptは事前に期待した以上の成果を上げています。
ときには型を当てることが難しい場合もありますが、⼀番⼤切なことは少しでもコードの品質という⾯で前進していることだと思います。今後もTypeScriptへの移⾏や型の厳格化を進め、Eightの品質の担保と開発効率の向上を図っていきたいと思います。
