! This post is also available in the following language. 繁体中国語

TypeScriptの導入にあたって考慮すべきメリットとコスト

皆さんこんにちは。京都開発室のLinです。仕事と個人的なプロジェクトでTypeScriptによる開発を始めて約2年が経ちましたので、 TypeScript導入時の経験を皆さんにお話ししたいと思います。

近年、TypeScriptはフロントエンド分野で最も注目を集める技術となっています。The State of JavaScriptの資料によれば、TypeScriptの使用を選択する開発者は増加を続けており、その評価も肯定的なものが多いようです。

「次のプロジェクトではTypeScriptにより開発を行うべきだ」「今あるJavaScriptプロジェクトをTypeScript仕様に変更すれば、プロジェクト品質の向上につながる」と考えているチームは数多くあるでしょう。

しかしながら、TypeScript導入のコストおよびメリットについては、極めて慎重に評価を行う必要があると、私は考えています。

TypeScriptの導入に必要なコストを軽視してはならない

これまでの個人的な経験からすれば、TypeScriptに習熟した後の開発効率は、JavaScriptを使用する場合とほとんど変わりがありません。

型宣言のためにプログラムコードは少し長くなりますが、それでも自動補完および定義クエリによって、ファイルとプログラムコードを照会する時間をある程度を減少させることができます。

リファクタリングが必要な時にも、JavaScriptプロジェクトに比べて時間が節約でき、安全です。

しかし、初めてTypeScriptを使用して開発を行うチームは、JavaScript による開発に比べて1~2倍の時間が余分にかかる覚悟を持たなければならない、というのが私の考えです。

学習コスト

「TypeScriptはJavaScriptに型表記を追加したバージョンにすぎず、学習は難しくないはずだ」と考えているのであれば、TypeScriptへの適応のためにチームとして支払わなければならないコストの見積もりを誤っている可能性が高いです。

JavaScriptを熟知した人間にとってみれば、静的型の制限があることによって、JavaScriptでは簡単に実現できていた設計がTypeScriptでは実現困難である、ということになります。また、型システムのために、TypeScriptにはその他の静的言語による設計との間にも多くの違いがあります。Java、C#またはHaskellなどの他の言語の使用経験がある人にとっては、むしろ折に触れ驚くことになるでしょう。

言語そのものに加え、既存のフレームワークやライブラリをTypeScriptに対応させる場合の使用方法がJavaScript の場合と異なっていることも学ばなければなりません。

実際には、入門から習熟までかなりの時間を要するのです。

ライブラリの型定義サポート

TypeScriptでJavaScriptライブラリを使用することは可能ですが、既存のJavaScriptライブラリでTypeScriptに必要な型情報が提供されているとは限りません。

厳密なコンパイル設定の下では、型情報の提供されていないライブラリを使用するとコンパイルエラーとなります。

比較的人気の高いライブラリでは、ほとんどの場合、コミュニティによってメンテナンスされているtypesの中から対応する型定義セットを見つけ出すことができます。しかしながら、第三者によって提供される型は必ずしも正確ではなく、また、元のライブラリのバージョンがアップデートされたにもかかわらず、必要な型定義セットはアップデートされていない、という状況も起こりえます。

型定義が誤っている、または不完全である場合は、むしろ型の誤りが原因でライブラリが誤用されてしまう、または型処理に長い時間がかかってしまう可能性があります。さらには、TypeScriptでサポートされていないために、実用的なJavaScriptライブラリの使用を諦めざるを得なくなる場合もあります。

一方、TypeScriptでサポートされているフレームワークまたはライブラリについても、TypeScript使用時には追加の設定を行う必要が生じることもありますし、APIの形式が異なっている場合すらあります。

例:

  • Vue2で、TypeScriptから質の高いサポートを受けるためにclass component形式を使用する。
  • Redux toolはTypeScript使用時に追加設定が必要となり、また一部のAPIにはJavaScriptと異なる形式が採用されている。
  • Emotionはtheme型取得のためにimportパスを変更する必要がある。

JavaScriptの常用モデルをうまく処理できない場合がある

JavaScriptで不特定のパラメータ、組み合わせモデルおよび高階関数などを頻繁に使用している場合、TypeScriptを使用すると数多くの障害に遭遇することになります。関連問題の処理を求める提言(#1213#16936)も、現在(TypeScript4.0)に至るまで解決されていません。

例えばlodashのflow関数は、TypeScriptで処理の難しいものの一例です。

このような問題のために、開発面では、元々JavaScriptで実現されていた柔軟性が失われ、設計モデルの一部を放棄するか、または多大な労力を費やして複雑極まりない型宣言を記述するかの選択を迫られることになります。

緩和と厳密性の間での苦闘

TypeScriptには数多くのコンパイルオプションがあります。また、ESLintなどのツールでもTypeScriptに対応するためのルール拡充が行われています。よく目にする提案内容は、anyの使用を禁じ、厳密なnullableチェックを行うなど、「最も厳密な設定を使用する」ことです。

しかし、実際の経験からすれば、初めてTypeScriptを使用する場合、最も厳密なコンパイルオプション下での開発は大変困難で時間がかかります。現段階でanyの使用または型変換なくしては解決不可能な問題も数多く存在します。

TypeScriptの十分な経験を有するメンバーがチーム内にいなければ、「いつ制限を緩和することができるか」の判断を下すのが大変困難になります。

開発においては、型定義問題の解決に繰り返し挑むものの、失敗して妥協を余儀なくされるかもしれません。または、Code Review時に「anyを使用しない他の解決方法はあるか」「この行に@ts-ignoreを加えるべきか」などの問題の研究・討論に多大な時間を費やすことになるかもしれません。

コンパイルのプロセス

TypeScriptはコンパイルして初めて実行可能となりますが、現在のコンパイラの処理速度は十分であるとはいえません。このため、プログラムを変更してから再テストが可能となるまでには一定の時間待つ必要があります。

TypeScriptを使用した有名プロジェクトであるDenoでも、コンパイル時間が長すぎるなどの関連問題のために、一部のTypeScript プログラムがJavaScript仕様に変更された経緯があります(#6793

TypeScriptのプログラムスタイルが好みに合うか否か

TypeScriptを使用した後の影響には、良し悪しを断定しにくいものもあります。つまり、結局は開発者の習慣と好みによって決まるのです。

例えば型の宣言は、型情報の理解に有用ですが、プログラムロジックの閲覧に影響すると考える人もいます。

TypeScriptを使用する場合は、型宣言を必ず追加しなければならないほか、ライブラリ使用時にも型定義のサポートのために比較的複雑な形式を用いざるを得ないことがあります。

型定義のサポートのためのかなり不自然なプログラム作成

ネイティブAPIを使用してpost中のすべてのimgタグのsrc属性を走査する場合を例に取ります。

document.querySelectorAll('.post img').forEach(image => {
  console.log(image.src)
// Error:           ^^^Property 'src' does not exist on type 'Element'
}) 

2行目ではimageによって取得される型がElementであるため、src属性の存在を確実に保証することができず、型エラーとなります。 

型の問題を解決するためには、型変換を追加で行う必要があります。 

(document.querySelectorAll<HTMLImageElement>('.post img').forEach(image => {
  console.log(image.src)
})

ただし、このような型変換は実際には決して安全ではありません。例えばimgdivに変更しても警告が出ません。 

document.querySelectorAll<HTMLImageElement>('.post div').forEach(image => {
  console.log(image.src)
})

追加の型変換を不要とするテクニックとしては、以下のようにquerySelectorAll(‘img’)を使用する方法があります。 

document.querySelectorAll('.post').forEach(post => {
  post.querySelectorAll('img').forEach(img => {
    console.log(img.src)
  })
})

このような記述にすればいっそう適切な型情報を得ることができますが、元のJavaScript形式ほど簡潔ではなくなります。 

TypeScriptプロジェクトでは、同様の理由で不自然なプログラムコードを作成しなければならない場合が多々あります。 

型宣言の占めるスペースがプログラムロジック部分よりも多くなる 

外部のライブラリによって提供される関数を使用し、一部のパラメータを固定して、別の関数としてパッケージ化する場合を想定すると、JavaScriptを使用した場合の典型例は以下のとおりです。 

import { sendSomething } from 'some-lib';

export function sendInJson(options) {
  return sendSomething({
    ...options,
    type: 'json',
  })
}

一方、TypeScriptでは、このライブラリからパラメータインタフェースがexportされていない場合、sendInJsonの型を正確に宣言するために、以下のような記述が必要となります。

import { sendSomething } from 'some-lib';

type SendSomethingOptions = Parameters<typeof sendSomething>[0]
type SendInJsonOptions = Omit<SendSomethingOptions, 'type'>

export function sendInJson(options: SendInJsonOptions) {
  return sendSomething({
    ...options,
    type: 'json',
  })
}

上の例では、プログラム文の半分程度が型情報の宣言のためだけに存在しています。より複雑な型を扱う場合は、この問題はいっそう深刻になります。 

TypeScriptによるメリットには価値があるか 

TypeScriptの使用を選択した場合、期待されるメリットには以下のものがあります。 

  1. 静的な型チェック。コンパイル時に一部の型エラーを検出することができます。
  2. より高性能なエディタ機能。リネーム、定義クエリおよび自動補完など。 
  3. 一目瞭然の型宣言。プログラムコードの読みやすさの向上。
  4. 他人が使用するためのライブラリを開発する場合は、型定義の提供によりユーザの開発エクスペリエンスを向上可能。 

TypeScriptによってプログラム作成時のエクスペリエンスは確かに向上し、リファクタリングもいっそう迅速かつ安全になると思います。これも多くの人が使用を推奨する理由です。しかしそのメリットが期待されるとおりであるか否かについては、さらなる評価が待たれます。 

静的型チェックは完全に安全という訳ではない 

静的型チェックを行えば、動的型チェックを減らすことができますが、動的型チェックに完全に代替できる訳ではありません。TypeScriptの静的型チェックがあるからといって、動的型チェックを行う必要がないという認識になってしまうと、かえって多くの問題が発生します。 

例えば、下記の例を使用してデータを取得する場合を考えます。 

const response = await fetch(dataSource)
const data: MyData = await response.json()

response.json()が返すのはPromise<any>型であるため、ここでは型エラーは発生しません。 

そのため、この後はdataを使用するどの部分も確実にMyDataに属すると思ってしまいますが、実際には見込んだとおりにならない可能性があります。 

また、anyの使用を禁止し、外部またはネイティブのAPI中のany型をそれぞれ適切に処理しても、TypeScriptの型システムも完全に安全なものではないのです(#9825)。 

良好な編集エクスペリエンスにTypeScriptが必要であるとは限らない 

自動補完についていえば、TypeScriptの記述により良好なサポートを得ることができます。しかし実際には、JavaScriptの記述のみでもそれは実現できるのですが、当然ながら、正確性はTypeScriptほど完全ではない可能性があります。 

例えばWebStormでJavaScriptを開発すると、素晴らしい推論機能を利用することができます。JavaScriptとJSDocを記述し、TypeScript Language Serverを利用することでも、TypeScriptの編集に近いエクスペリエンスが得られます。 

利便性はやや劣りますが、導入コストを大幅に削減することができます。 

リソースが有限である状況下では最良の投資であるとは限らない 

「TypeScriptでプロジェクトのソフトウェア品質が向上する」という考えを抱いてTypeScriptを導入するチームは多いことでしょう。 

しかし現実には、ソフトウェア品質について完璧を目指すのは難しく、労働力と時間の制限の下での最良を追い求めることになります。ソフトウェア品質を改善することができる手段は数多くあり、TypeScriptの導入はその選択肢の1つにすぎません。必ずしも最も価値のある投資であるという訳ではないのです。 

経験不足である場合は、TypeScriptの導入コストを見誤り、逆効果となる可能性すらあります。 

例えば、TypeScript関連の問題に時間をかけ過ぎると、下記の事態を招くことになります。 

  • アーキテクチャーの考慮に使える時間が圧迫される。
  • テストとデバッグの時間が不足する。
  • スケジュールの見込み違いにより、後半の開発作業が慌ただしく、ぞんざいなものになる。 
  • 他の静的分析ツールによって検出された問題を処理する余力がなくなる。  

そうなれば、取引自体が実際は割に合わないものとなります。 

結論 

TypeScriptの導入時に、参考となる評価項目には、以下のものがあります。 

  • 製品の開発スケジュールおよび将来のメンテナンス計画。 
  • チームメンバーがTypeScriptに十分習熟しているか否か。 
  • チームが静的型言語の使用を好んでいるか否か。
  • 使用する一連の技術がTypeScriptの良好なサポートを受けられるか否か。
  • エンジニアの学習に対して会社が進んで提供するリソース。 
  • ライブラリとして他人が使用するためのプロジェクトであるか否か。

現在、フロントエンド技術界では、「TypeScriptを使用することは現代のトレンドである」という傾向があり、TypeScriptの積極的な導入こそが時代の流れであるように見受けられます。 

しかしながら、TypeScriptは一部の問題を解決することはできても、その分余計な困難をもたらします。今日のTypeScriptは2年前と比べて少なからず進歩を遂げていますが、開発時に困難が生じる問題は依然として数多く発生しています。今後は、これらの問題が徐々に改善され、使いやすさが向上することも期待されます。 

TypeScriptは、現段階では、学習する価値はあるが必須の技術という訳ではない、と私は考えています。チームとしての需要および能力に応じ、慎重に評価した上で選択を行うのがよいでしょう。