こんにちは。この記事は筆者が開発した「better-typescript-lib」を宣伝する記事です。これは、導入するだけでTypeScriptプログラムがより型安全になるという素晴らしいライブラリです。あくまで型定義なので、導入してもランタイムの挙動は何も変わらず、バンドルサイズなどへの影響もありません。
better-typescript-libの導入法
ここに記載されているのはv1 (TypeScript 4.0 〜 4.4向け)のインストール方法です。v2 (TypeScript 4.5〜)ではインストール方法が変わり、最初のnpm installのみで良くなります。詳しくは次の記事をご覧ください。
GitHubにも書いてありますが、ここにも記載しておきます。まずbetter-typescript-lib
をインストールしましょう。
npm i -D better-typescript-lib
次に、tsconfig.json
を編集してlib
を除去し、代わりにnoLib
をtrue
にしましょう。
- "lib": ["es5", "dom"]
+ "noLib": true
最後に、プログラムのどこか(エントリーポイントとなるファイルが良いでしょう)に次のような記述を追加してbetter-typescript-libを読み込みましょう。何を読み込むかは、もともとのlib
の記述と同様にします。
/// <reference path="./node_modules/better-typescript-lib/lib.es5.d.ts" />
/// <reference path="./node_modules/better-typescript-lib/lib.dom.d.ts" />
以上で終了です。
better-typescript-libは何をしてくれるのか
better-typescript-libは、TypeScriptの標準ライブラリをより安全性が高くなるように修正してくれるものです。TypeScriptの標準ライブラリとは、JavaScript及びDOMにもともと備わっている機能の型定義のことです。例えばparseInt
とかJSON.parse
とかdocument.querySelector
といったものは特に追加のライブラリを導入しなくてもTypeScriptプログラムで使用可能でちゃんと型が付きますが、これらに型が付くのはTypeScriptコンパイラに付属する標準ライブラリが型定義を提供してくれているからです。
TypeScriptの標準ライブラリは、歴史的経緯や利便性の重視などの理由により、完璧に型安全でない点がいくつもあります。代表的なものは、JSON.parse
の返り値がany
型となっていることです。
type FooObj = { foo: number };
const obj: FooObj = JSON.parse("{}");
// 本当はundefined😨
const num: number = obj.foo;
このように、JSON.parse
の結果には好き勝手な型を付けることができてしまうため、実態と異なる型となり型安全性を破壊するのが非常に簡単です。
better-typescript-libを導入すると、JSON.parse
の返り値の型はany
ではなくJSONValue
型に修正されます。その結果、安全性が保証されない代入に対してこのように正しくコンパイルエラーが発生します。
type FooObj = { foo: number };
// エラー: Type 'JSONValue' is not assignable to type 'FooObj'.
const obj: FooObj = JSON.parse("{}");
better-typescript-libの存在下ではJSON.parse
を使うために取れる選択肢は2つです。io-tsのようなランタイム型チェックライブラリと組み合わせることで完全な安全性を得るか、あるいはas
を使ってJSONValue
型を目的の型に変えることです。
type FooObj = { foo: number };
const obj = JSON.parse("{ \"foo\": 3 }") as FooObj;
const num: number = obj.foo;
JSON.parse
によって得られる型が(人間にとっては)明らかな場合にはas
を使うのも有効でしょう。その場合でも、従来はany
によって隠されていた危険性がas
という形で明確に現れるようになったのは大きな進歩です。
このように、より安全な型定義は、プログラムのバグを未然に防ぐのはもちろん、危険なプログラムを書くことに対する責任をTypeScript(の標準ライブラリ)から人間の側に移します。これによりプログラムの安全性をプログラマがより精密にコントロールすることができるようになり、本質的に危険な操作が必要な場合でもそれがどこで発生しているのかを把握・注視することができます。
better-typescript-libの安全性の他の例
better-typescript-libでは、筆者がTypeScriptの標準ライブラリを見て目についた危険性を一通り修正してあります(この記事執筆時点ではlib.dom.d.ts
にはまだ手をつけていませんが)。多いのはany
をunknown
に修正するようなものです。
例えば、TypeScriptの標準ライブラリではnew Array()
がArray<any>
となってしまいます。また、new Array(10).fill(123)
というよく使われるパターンも同様にArray<any>
となり危険です。better-typescirpt-libを導入することで、これはより安全なArray<unknown>
という型になります。後者のコードはnew Array<number>(10).fill(123)
とすればArray<number>
になります。
なぜbetter-typescript-libが必要なのか
TypeScriptの標準ライブラリに型安全性の問題があるならば、本来は安全性を直すのはTypeScript本体側であるべきです。これまでも、標準ライブラリの修正が全く行われなかった訳ではありません。
しかしながら、その際には後方互換性の問題が付きまといます。特に、先ほどのJSON.parse
がany
を返す件などは深く考えずに利用してしまっている人が多いでしょう。このような影響範囲の大きい変更は行われない傾向にあります。以前には、Promise.all
の型定義を改善した結果後方互換性の問題が発覚し、リバートされたという例もあります。
過去には、コンパイラオプションが指定されたときのみより安全な型定義が適用されるようにした例もあります(--strictBindCallApply
)。ただ、これもコンパイラオプションの有り無しによって異なる型定義が読み込まれるような実装にはなっておらず1、そのような実装はアーキテクチャ上難しいようです。また、コンパイラオプションをむやみに追加するのは望ましくないことから、anyを1つや2つunknownに変える程度の修正を行うのも難しいでしょう。
そこで、改善された型定義を別のライブラリに切り出すことで、TypeScript本体の後方互換性に縛られずに柔軟な運用が可能となります。それがbetter-typescript-libの役割です(切り出すという書き方をしましたが、better-typescript-libはTypeScript公式とは何の関係もない非公式のライブラリです)。
また、TypeScriptは今も進化を続けており、今年(2020年)もVariadic Tuple TypesやTemplate Literal Typesと言った大胆な機能追加が行われました。既存の型定義の中にもこれらの機能を使って書き直せるものがあるはずですが、前述の後方互換性の問題からむやみに型定義の改善は行われません。これもbetter-typescript-libが拾うことができる役割です。例えば、TypeScriptの型定義にはPromise.all
に引数の数に合わせて10個くらいのオーバーロードがありますが、better-typescript-libではTypeScript 4.0で追加されたVariadic Tuple Typesを用いてこれを1つだけにしました。
ところで、タイトルに「絶対入れるべき」と書きましたが、一つ例外があります。それは、あなたが別に型安全性にこだわらない場合です。例えばJSON.parse
の型はany
のほうが便利なので嬉しいと思う場合は、better-typescript-libを導入する必要はないでしょう。
逆に、できる限りの型安全性を保ちながらTypeScriptを使いたいという場合は、ぜひbetter-typescript-libを導入するべきです。理由は単純で、better-typescript-libを入れたほうがより型安全だからです。
better-typescript-libを使う際の注意
そんな絶対入れるべきなbetter-typescript-libですが、いくつか紹介しておくべき注意点があります。
1. TypeScriptのバージョンを上げる際はbetter-typescript-libも対応バージョンに上げる
TypeScriptのバージョンを上げた際は新しい型定義が標準ライブラリに追加されることがあります。better-typescript-libをそれを取り込んだバージョンに上げないと、新しい機能が使えない場合があります。
また、better-typescript-libがTypeScriptの新バージョンに対応するまで待つ必要があるかもしれません。
2. better-typescript-libのバージョンを上げると新たなコンパイルエラーが発生することがある
TypeScript本体が後方互換性に縛られている分、better-typescript-libはより高い頻度で改善を行いたいと考えています。そのため、better-typescript-libのバージョンを上げる際は、改善の結果発生した新たなコンパイルエラーに対処する必要があるかもしれません。
型安全性のためなら喜んでコンパイルエラーに対処しますという志のある方なら、better-typescript-libの使用を妨げるものは何もないでしょう。
3. 既存のコードベースに導入する際は多少の作業が必要
better-typescript-libは新しいTypeScriptプロジェクトに導入するのが最も向いていますが、既存プロジェクトでも使いたいという場合は導入が可能です。その場合、大抵の場合はbetter-typescript-libの導入によってコードの危険な部分が炙り出され、コンパイルエラーが発生するでしょう。これらに対処する必要があります。
コードの性質にもよりますが、筆者が約10万行のコードベースで試してみた際は30個程度のコンパイルエラーが発生しました。
まとめ
この記事では、TypeScriptの標準ライブラリをより型安全にするライブラリ「better-typescript-lib」を宣伝しました。
「JSON.parse
の返り値がany
」はTypeScriptの間の抜けたところを表す鉄板ネタでしたが、2021年からはそれは通用しません。better-typescript-libがあるのですから。
-
安全な型定義を持つ新しいインターフェース
CallableFunction
を用意し、関数リテラル等の型がそちらに差し変わるという実装になっています。 ↩