uhyo/blog

どのようにTypeScriptを使うのか

2021年10月23日 公開

現在、TypeScriptの重要性は、フロントエンド開発を中心としてますます増すばかりであります。それだけに、TypeScriptをどのように使うべきかという問題については多様な意見が見られます。

これまで筆者はTypeScriptの使い方に、特にコンパイラオプションの使い方について意見を散発的に発信してきましたが、このたび記事にまとめました。この記事では、特に次のような意見に対しての反対意見を述べます。

  • 厳しいコンパイラオプションは型パズル愛好者のためのものであり、普通の人は細かいことを気にせず緩い設定でよい。
  • 熟練のJavaScript使いにはTypeScriptは必要ない。

例え話

最近はTypeScriptを補助輪に例えたりするのが流行っていますので、この記事でも例え話をしてみます。筆者の考えでは、TypeScriptというのは例えるならば料理人が使う包丁のようなものです。コンパイラオプションが色々あるのは、場面に応じて適した包丁が10本くらいある状況です。

この例えでいくと、上の2つの意見は次のように言い換えられるでしょう。

  • 包丁を使い分けるのは料理マニアのやることであり、普通の料理人は出刃包丁1本でよい。
  • 熟練の料理人は子供用の包丁で何でも作れる。1

いかがでしょうか。こう書くと、あなたが賛成かどうか決めやすいかもしれません。おおよそ「プロは道具を選ばない」派と「プロは道具を使いこなす」派に分かれるのではないでしょうか。筆者は後者です。

この例え話を通じて最も伝えたいことは、TypeScriptは道具であるということです。特に、TypeScriptの型チェック機能を通してコードのバグを未然に防ぐ、コードの安全性保証のために使う道具としての側面をこの記事では重視します。TypeScriptも完璧ではないので「保証」という言葉は強いかもしれませんが、便利な言葉なので使っています。TypeScriptの型システムでサポートされている範囲については保証してくれるものだと理解してください。

以降は「TypeScriptは道具である」という考え方をベースに論を組み立てていきます。たまに、TypeScriptを枷だと思う人がいますが、そのような見方は目的意識を希薄化してしまうのであまりおすすめしません。あなたのJavaScriptプログラミングにおいて、バグを未然に防ぐのを補助してくれる道具として、あなたの目的のためにTypeScriptを使うのです。良くある「JavaScript vs TypeScript」のような議論も、別々のプログラミング言語の比較というよりは「TypeScriptという道具を使うかどうか」という話だと理解することをおすすめします。

各論 (1) コンパイラオプションをどうするか

TypeScriptを採用したとしても、コンパイラオプションをどうするかというのは意見が分かれるところです。筆者の意見は、可能な限り厳しいオプションを使用せよです。もちろん、特殊な事情がある場合は別です(既存のJavaScriptコードをTypeScriptに移行する必要がある場合や、既存のプロジェクトでTypeScriptのバージョンを上げる場合など)。

TypeSrciptを道具と考える見方では、コンパイラオプションはあなたが目的に沿って主体的に決めなければいけません。コンパイラオプションに興味を持たないということは、TypeScriptという道具をどのように使うのかということ(すなわち、あなたがTypeScriptを使う目的)に興味を持たないということであり、本末転倒です。

幸いにも、TypeScriptはコンパイラオプションの数こそ多いものの、難しく考える必要はありません。コンパイラオプションを制することが道具を使いこなすのに必要であることをあらわす比喩として10本の包丁を例え話に挙げましたが、実は実際のTypeScriptではもうちょっと話が簡単です。というのも、大部分のオプションは、より安全かどうかというたった一つの軸で判断できます。コンパイラオプションを有効にするとより安全で、有効にしないと安全ではありません(すなわち、特定のケースで安全性が保証されなくなります)。

このことを「コンパイラオプションを有効にすればするほど縛りが厳しくなる」と表現できるかもしれません。そうなると、「自分は自由に書きたいしそんなに厳しくなくて良いかな……」と思ってしまいがちですが、それは踏みとどまってください。コンパイラオプションを無効にして設定を緩くして得られる自由とは、危険なコードを書く自由です。つまり、安全性に関わるコンパイラオプションを敢えて無効にするということは、危険なコードを書くという方向への能動的なアクションとなってしまいます

TypeScriptを黙らせたいと思ったら

「そうは言ってもTypeScriptは頭が悪すぎて、俺が書いた完璧に安全なコードに文句を言ってくる。自由に書かせろ」といった反論は考えられます。実際、安全なコードなのにTypeScriptがうまく型推論できなくてコンパイルエラーが出てしまう場面はあります。

しかし、そういった場面で取るべき行動は、コンパイラオプションを無効化することではありません。そうではなく、anyasやユーザー定義型ガードといった危険な道具を使うことです。なぜなら、コンパイラオプションはプロジェクト全体に影響を与えるからです。コードをなるべく安全に保ちたいならば、危険な道具の影響範囲をあの手この手で最小限にとどめることが必要であり、そのためにはasなどのほうが適しています。

歴史的な事情

そもそも、TypeScriptに色々なコンパイラオプションが用意されている理由は、歴史的に見ればもっぱら後方互換性のためです。

TypeScriptは進化の過程で型チェック力を増し、その安全性を増してきました。よりチェックが厳しくなることは、TypeScriptを使用している既存のプロジェクトから見たら破壊的変更となります。以前はコンパイルエラーが出なかったコードに対して、TypeScriptのバージョンを上げるとコンパイルエラーが出てしまうからです。

小さめの破壊的変更は普通にリリースされますが、既存のコードへの影響が大きいと思われる大きな改善はコンパイラオプションの形でリリースされ、コンパイラオプションを有効にした人だけ新しい改善が適用されるという戦略がとられます。最近の例としては、TypeScript 4.4でリリースされたuseUnknownInCatchVariablesオプションが挙げられます。自分のコードが新しいチェックに耐えられない場合は、このオプションを無効化することでひとまずコンパイルエラーを消すことができます。もちろん、これは従来検出されなかった危険性がTypeScriptの新機能によって炙り出されたということですから、長期的にはオプションを有効化できるようにコードの改善を進めていくべきであることは言うまでもありません。

つまるところ、安全性に関わるコンパイラオプションが導入された動機は、新しい安全性レベルに適合できない既存コードのコンパイルを通すためであり、安全性レベルの選択肢を与えることは元々の意図ではありません。特に、新規のTypeScriptプロジェクトでは、その時点で利用可能な安全性オプションをわざわざ無効にする理由がありません。だからこそ、敢えて選択肢として解釈しても、上記のように「危険なコードを書く自由」といった魅力のない選択肢にしかならないのです。

関数引数の型を書きたくない

TypeScriptのコンパイラオプションについては上記のように説明しましたが、実際のところほとんどのコンパイラオプションについてはあまり議論になるのを見かけません。議論の的となる代表的なコンパイラオプションはnoImplicitAnyです。このオプションは、いくつかの危険なシチュエーションにおける挙動を指定するもので、オプションを有効にするとコンパイルエラーとなる場面でも、オプションを無効にするとany型が推論されてその部分の型推論が無効化されるので、コンパイルエラーになりません。

noImplicitAnyを無効化した場合に発生する大きな違いは、関数引数の型を書かなくても良くなることです。

// noImplicitAnyを無効にするとコンパイルエラーにならないコード
// (arrに対する型チェックは行われなくなる)
function mapToSign(arr) {
  return arr.map(x => x >= 0);
}

// noImplicitAny有効下ではこうする必要がある
function mapToSign(arr: readonly number[]) {
  return arr.map(x => x >= 0);
}

TypeScriptでは、型推論が働くためには関数引数の型を明示的に書く必要があります。これは推論力が足りないというよりは、TypeScriptがそのようなデザインを採用しているということです。関数型言語を中心に、引数の型を書かなくても関数の使われ方から推論してくれる言語もありますが、TypeScriptはそうではありません。おそらく、型チェック速度を遅くしないためというのが最も大きな理由でしょう。

このことが気に入らない人が、noImplicitAnyの無効化(さらには、そもそもTypeScriptを使わないこと)を支持する傾向にあるというのが筆者の印象です。曰く、十分能力がある人なら引数の型をわざわざ書かなくても十分安全なコードにできるというのです。

これに対する反論としては、次の言葉を引用します。

生JavaScriptでも、たとえば「この変数には数値だけが入っているはずだ」みたいな意識が生じたらもうそこには型システムが存在していて、あとはその人の頭の中だけにある貧弱で曖昧な型システムを使うか、みんなが使ってて自動的に検証できるTypeScriptの型システムを使うか、という違いしかないです

https://twitter.com/cubbit2/status/1430923767643992065

noImplicitAnyが無効の状態で引数の型を書かなかった場合に起こることは、引数がany型となり、その引数に対しては安全性保証がされなくなるということです。引数の型を書かなかったのをいいことに、TypeScriptがその部分の安全性保証をサボってしまうのです。そうするとコンパイルエラーは確かに出なくなりますが、依然として「引数として何が渡されるか」といったことは考慮する必要があります。なぜなら、そうしないとバグは結局発生してしまうからです。

上のコードでも、型が書いていないとはいえ引数arrに配列以外を渡してはいけません。そうするとコンパイルエラーは出ないもののランタイムエラーになってしまうからです。つまり、何らかの形で「mapToSignに配列以外を渡さないこと」をケアしてあげる必要があります。具体的な方法としては、例えばmapToSignを使っている箇所を全部見て回って目視で確認するといったことが挙げられます。

もしあなたがこのようなケアを行うのならば、それはコードの安全性を気にしているということです。このように、TypeScriptでは、型を書かないということはコードの安全性をあなたの責任において保証してあげる必要があるということを意味します。そのためにあなたが脳内で思考したことは、実質的にTypeScriptの型チェッカーが普段やっていることと同じです。

つまり、「安全なコード」というゴールを変えない限り、型を書くか書かないかというのは、安全性の保証という仕事を人間がやるかTypeScriptがやるかの違いにしかなりません2

そして、ほとんど全ての場合において、型チェックによる安全性の保証というタスクはあなたの脳内型チェッカーよりもTypeScriptの方がうまくやってくれますから、TypeScriptに任せるべきです(残りのレアケースにおいては人間が責任を負う必要があり、asなり何なりを使うことでTypeScriptと協調しながらできるようになっています)。

型にあまり魅力を感じない人にとっては、noImplicitAnyを無効にすることは「わざわざ型を書かなくて良くなる」という点で魅力的に見えるかもしれません。しかし、(バグをなるべく防ぎたいという前提がある限りは)実際には結局あなたの脳内型チェックが必要です。それはわざわざTypeScriptの仕事を減らして自分の仕事を増やす行為であり、素直に型を書くのに比べて100倍くらい大変なことなのです。

コードに対する3つの選択肢

結局のところ、TypeScriptプロジェクトにおいては、その中の全てのコードに対して次の3つの選択肢が与えられていることになります。

  1. TypeScriptに型チェックをして安全性を保証してもらう。
  2. 人力で型チェックして安全性を保証する。
  3. 安全性を保証しない(型の問題に起因するバグを防ぐことを諦める)。

TypeScriptには「徐々に型を導入できる」という謳い文句がありますが、これはコードの各部分ごとに上記のどれを選択するか選べるということです。この見方ではnoImplicitAnyはデフォルトを安全側に倒すか危険側に倒すかの違いと解釈できます。

TypeScriptが無い時代、JavaScript使いには最初の選択肢がありませんでした。静的型システムの無い言語を書く人にとっては、今でもそうです。バグを防ぎたい場合、人間の責任において安全性を担保することになります。これは、TypeScriptが何もしてくれない状態、すなわちTypeScriptが保証してくれる安全性がゼロの状態と解釈できます。

上記の説明から分かるように、noImplicitAnyを無効にすると、TypeScriptが保証してくれることが減って、安全性が必要ならば人間がそれを補うことになるので、「TypeScriptが何も保証してくれない」という状態に近づきます。このことから分かるように、noImplicitAnyも他のコンパイラオプションと同様に、無効にするとTypeScriptによる安全性保証が低下する類のものです。他のコンパイラオプションと同様に、後方互換性といった事情が無い限り、無効にする理由が無いのです。

もしあなたが安全性の重要度が低いプロジェクトに従事しているのであれば、「型を書かない」ということが3の選択肢(安全性を保証しない)ことの能動的なシグナルとして有効かもしれません。このような意思決定を明示的に行うのであれば、noImplicitAnyを無効にする理由となるかもしれず、そこまで理解して意思決定しているのであれば筆者もそれを止めません。

ただし、noImplicitAnyが有効のままでも、引数の型をanyと明示的に書くことによって、引数の型を書かなかったのと同じ意味になります。もしプロジェクト全体としては安全性が必要だが部分的にここの安全性保証は放棄したいという場合には、anyと明示的に書くことを強くおすすめします。コードを書く人が、自分はTypeScriptによる自動的な安全性保証を放棄するのだということを明示的に理解していることは、忘れずに上記の選択肢3ではなく選択肢2(人力で安全性を保証する)を取る助けとなるからです。

noImplicitAnyが有用なシチュエーションとは

少し前でも触れましたが、noImplicitAnyを無効にすることは「JavaScriptのコードをTypeScriptに移行したい」という特定のシチュエーションにおいては有用です。これはまさにTypeScriptによる安全性の保証がゼロの状態から安全性を高めていかなければならない状況であり、既存のJavaScriptコードには型なんて書いてありません。途中のマイルストーンとして「noImplicitAnyが無効ならコンパイルが通る」という目標を設定することは合理的でしょう。

ただし、noImplicitAnyが無効の状態をゴールとすることにはとても慎重にならなければいけません。TSへの移行といった特別な事情を抜きにして考えれば、この記事で述べたように、noImplicitAnyを無効にするというのはTypeScriptが保証してくれる安全性を敢えて低下させるという能動的な選択に他なりません。

そのようなことは、「かかる労力とのトレードオフ」といった外的な事情によってはじめて正当化されます。というのも、noImplicitAnyが無効の状態から有効の状態に持っていくことは、非常に大きな労力がかかります(筆者の会社では筆者が属するチームが実際にこれを行ないましたが、1年以上かかりました)。そこまでして得られる安全性保証と、そのための労力を天秤にかけることで、「安全性を諦める」あるいは「自動化の恩恵を受けず従来通り人力で頑張る」という選択が正当化されうるのです。

逆に言えば、新規のプロジェクトを立ち上げる場合にはそのようなしがらみが無く、noImplicitAnyの無効化を正当化することはとても困難です。「型を書く労力」と安全性とを天秤にかけたくなるかもしれませんが、前述の通り、安全性保証が必要であるという大前提がある限りは、型を書かなくても人間が安全性保証をしてあげる必要があります。人間が安全性保証をするときも、何もヒントが無いよりは「この関数にはこんな引数を渡すべきである」といった情報があるべきでしょうし、それはもはや型です。あとは人間(脳内型チェッカー)だけが理解できる方式で書くか、TypeScriptで理解できる方式で書くかという違いでしかありません。そう考えてみると、天秤に載せた「型を書く労力」の重さは、多くの場合結構軽いものであるはずです。

厳しいオプションは誰のためか

これまでに説明した通り、TypeScriptで安全性のためのコンパイラオプションを有効化すると、チェックが厳しくなります。「厳しい」と言われると、特にTypeScript初心者の方は、「初心者がいきなり厳しい設定にしなくても良いのではないか」と思うかもしれません。しかし、それは典型的な誤解です。むしろ、初心者こそもっとも厳しい設定にすべきです。

これまで述べてきたように、TypeScriptを使用するのは、型チェックにより安全性を保証するためです。そして、TypeScriptを使う目的はもちろん、バグを減らすためです。

厳しい設定を無効化するということは、TypeScriptによる安全性保証の精度を低下させるということです。つまり、端的に言えば(期待値的に)バグが増えるということです。もしバグが増えるのが望ましくないのであれば、TypeScriptの力が低下した分を人間が補ってあげる必要があります(上記でいう人間による型チェックです)。

では、TypeScriptが最大限の力を発揮してくれるのと、TypeScriptがやってくれることの一部を人間が肩代わりしなければいけない場合とでは、どちらが初心者向けでしょうか。言うまでもなく、前者です。

したがって、なるべくコンパイラオプションを厳しくするほうがむしろ初心者向けであると言えます。

さらに言えば、熟練のエンジニアだろうと、コンパイラオプションを緩くする理由はありません。コンパイラオプションを無効化にして得られるものは「危険なコードを書く自由」であると説明しましたね。確かに、難しいプログラムでは、ある意味で「危険」なプログラム、つまりTypeScriptが安全であると認識してくれないプログラムを書く必要があります。しかし、そのための手段はコンパイラオプションを無効化することではありません。なぜなら、それは大域的な手段でありプロジェクトへの影響が大きいからです。

TypeScriptが安全性保証できる範囲を逸脱する必要がある場合、すなわち人間の責任による安全性保証が必要になった場合、適切なのはasなどといった危険な型・構文です。これらの持つ危険性は言わば局所的な危険性であり、工夫次第でその影響範囲を狭くできるという特徴を持ちます。局所的な危険性を制御し、プロジェクト全体への影響を最小限にし、TypeScriptの安全性保証を最大限活かすことが熟練のTypeScriptエンジニアに求められる所作です。

以上のことから、冒頭で紹介した以下のような意見に筆者が否定的である理由がお分かりになるでしょう。

厳しいコンパイラオプションは型パズル愛好者のためのものであり、普通の人は細かいことを気にせず緩い設定でよい。

各論 (2) JSDocでよくない?

TypeScriptは、実はJSDocに対するサポートにも力を入れています。型註釈の構文が使用できない.jsファイルであっても、JSDocコメントを通じて型を書くことで、型チェックの恩恵を受けられるという機能です。

/**
 * @param {readonly number[]} arr
 */
function mapToSign(arr) {
  return arr.map(x => x >= 0);
}

TypeScript不要論の変種として、型チェックの恩恵は受けつつ、TypeScriptに特有の構文を避けてJSDocで全てを書くべきであるという主張も最近見かけられます。これに対しても筆者の意見は否定的です。

JSDocに寄せることのメリットとその考察

JSDocに寄せることのメリットは、トランスパイルが必要なくなることです。これが唯一のメリットと言っても過言ではなく、筆者は他のメリットが思いつきません。

そもそも、TypeScriptの文法はJavaScriptを型註釈のための構文で拡張したものです3が、これはJavaScriptではないため、ブラウザやNode.jsといった処理系でそのまま動かせません4。そのためJavaScriptへの変換作業が必要で、これがトランスパイルと呼ばれています。

トランスパイルの問題として主に2つあり、一つはトランスパイルに時間がかかること、そしてもう一つは環境構築・維持が大変なことです。このうち、トランスパイルに時間がかかるという点についてはesbuildの登場によってかなり改善されています。

環境構築の問題

ここで取り上げるトランスパイルの問題点は、環境構築の手間がかかることです。トランスパイルをするためのツール(tscなど)をインストールしたり、それを自動で実行したりするための環境を整える必要があります。

その手間が必要ないことがTypeScriptをJSDocで使うことのメリットとして知られているものですが、筆者はやはり懐疑的です。

特にフロントエンド全般は、なかなか環境構築に手間がかかる分野であることは否めません。バンドラーなどを開発に取り入れるとビルドフローが複雑化します。

しかし、実はTypeScript単体では大した手間にならないというのが筆者の考えです。最も単純なトランスパイル作業とは、.tsファイルをトランスパイラに入力して.jsファイルを得ることです。そのための環境構築は、慣れている人ならばtsconfig.jsonのセットアップを含めても5分とかからないでしょう。設定ファイルも、JSONファイルひとつだけです。

さらには、最近のVSCodeの頑張りにより、自分のローカル環境ではなくリモート(SSHでアクセスする別のホストや、あるいは自分のマシンにあるDockerコンテナ内など)に開発環境を整えることができるようになりました。これにより、環境構築の自動化が加速しています。このことから、環境構築のコストは相当減少しています。

開発環境の維持管理コストという観点もあるかもしれませんが、それはTypeScriptのバージョンを管理するだけです。TypeScript使いならばそれくらいできるべきだと正直なところ思います。包丁の例えで言えば、自らの道具をメンテナンスすることも料理人に必要なスキルだということです。

トランスパイルしない環境はどんなものか

TypeScriptをトランスパイルするためのコストは問題にならないくらい低いというのが筆者の意見ですが、それでもトランスパイルできないという環境があるかもしれません。ただ、それはnode.js環境を用意できないという主張に等しいものです。実際のところ、node.js環境を用意するのは大変だがVSCodeのインストールくらいはできるというケースはありそうです(VSCodeも裏ではnode.jsを使用していますが、パッケージ化されていてそれを感じさせません)。

ただ、そのような環境(VSCodeだけ)でTypeScriptを活用する場合、プロジェクトが非常に小さいか、あるいはそもそも安全性の保証が目的になっていないケースが多いと思います。なぜなら、VSCodeでは確かに編集中のコードのコンパイルエラーを表示することができますが、プロジェクト全体の型チェックを動かしてその結果を表示するのが難しいからです。後者のケースでは、VSCodeのJSDocサポートを、安全性の保証というよりは編集支援として使用していることになります。

裏を返せば、一定の大きさがあるプロジェクトで、編集支援だけでなく安全性の保証を目的とするのであれば、VSCodeだけというのは無理があるでしょう。node.js環境を用意して型チェッカーを走らせる必要があります。

利用できるツールに制限があり、TypeScriptをそもそも使えなくてVSCodeをインストールするのが精一杯というエクストリームな環境もあるでしょう。その環境でJSDocオンリーの運用は確かに役に立つかも知れませんが、安全性保証の程度という点ではちゃんとTypeScriptをインストールしたプロジェクトと同じ土俵に立ってはいません。前提が違うというやつです。それは環境ゆえに仕方がないことではありますが、仕方がないからそうするのであって、常にJSDocで頑張るべきというわけではありません。料理人がすごく狭い台所で料理を作れたとしても、レストランの厨房が狭い台所であるべきという話にはならないのです。

JSDocの構文の評価

TypeScriptの構文を使わずにJSDocを常用することは、筆者としては推奨しません。なぜなら、JSDocの構文はむやみに長くて構造化が十分ではないからです。例えば、同じ意味の型註釈をTypeScriptの構文とJSDocの構文で書いてみます(JSDocはTypeScript Documentationから引用)。

// JSDoc
/**
 * @template {string} K - K must be a string or string literal
 * @template {{ serious(): string }} Seriousalizable - must have a serious method
 * @param {K} key
 * @param {Seriousalizable} object
 */
function seriousalize(key, object) {
}

// TypeScript
function seriousalize<
  K extends string,
  Seriousalizable extends { serious(): string }
>(key: K, object: Seriousalizable) {
}

JSDocの側は、コメントとタグという形式に縛られている都合から、@param@templateといったタグを毎回書かなければいけないなどとても冗長です。構造化が十分ではないというのは、例えばひとつの引数の情報がTypeScriptでは1箇所にまとまっているのに対して、JavaScript + JSDocでは2箇所に分かれていることを指します。

プログラムの読み書きしやすさのためにきちんと構造化された簡潔な構文が必要であるというのは、Rustなどの最近の流れを見ているとどうも正しいように思われます。JSDocによる書き方はその逆を強制します。よその言語はよその言語だと思われるかもしれませんが、JavaScriptでもES2015でのアロー関数の導入や{ foo }といったオブジェクトリテラルの省略形の導入など、書きやすさ・読みやすさのための処置が取られています。最近議論されているパイプライン演算子などもその類でしょう。

以上のことから、「コメントで書ける」ことを売りにしたJSDocの記法は、プログラムとしての読み書きしやすさとしてはTypeScriptの構文よりも劣るというのが筆者の意見です。トランスパイルが必要ないという点のみで、JSDocはTypeScriptの構文に張り合っています。

まとめ

この記事では、「TypeScriptのコンパイラオプションはどのように決めるべきか」という問いに対する筆者の考え方をご紹介しました。

コンパイラオプションについては、「よく分からないけど人々が言い争ってるもの」などと思っている方がいたとしたら、それはやめましょう。TypeScriptという道具をあなたがあなたの目的のために使う以上、コンパイラオプションはあなた自身があなたの目的にしたがって主体的に設定すべきものである、というのが筆者の考えです。その考え方に従うと、最も厳しい設定にする以外の選択肢を取る理由は極めて希薄なものとなるでしょう。

記事の後半では、関連する話題として、TypeScript不要論の中でも最近話題になっていた「JSDocで良い」論について筆者の考えを紹介しました。


  1. この記事の初出時は「おもちゃの包丁」を例に出していましたが、よく考えるとおもちゃの包丁ではそもそも切れないような気がしたので、最低限料理に必要な切る機能を備えた道具という意味で「子供用の包丁」という例えに変えました。
  2. 型を必要としない側の主張としては「ユニットテストをしっかり書けば大丈夫」ということもよく聞かれますが、一般に型チェックで保証できる範囲の安全性については、ユニットテストよりも型チェックのほうが信頼できます。なぜなら、後者は理論的裏付けに基づく保証であるのに対して、前者は普通そうではないからです。また、ユニットテストを型チェックの代わりにしようとするならば、「このユニットテストで型チェックの代わりになっていること」を人間の責任で保証してあげる必要があり、あまり人間の仕事が減っていません。さらに言えば、筆者の意見としては、どうせ人間がテストを書かなければいけないなら、その労力を型を書くことに向けたほうが効果的だと思います。
  3. enumといった例外があるのはご存知の通りですが、これは歴史的経緯の産物であるため本質ではなく、ここでは省略しています。
  4. Denoならそのまま動かせるよと思った読者の方がいるかもしれませんが、Denoも裏でトランスパイルしてからJavaScript処理系にかけているので本質的には同じことです。