以下のルールは、実際のTypeScriptのコードでいちばん多くのバグを防ぐものです。それぞれ、よく見かける書き方を先に、よりよい書き方をあとに、実行できるコードで示します。最も重要なのは最初のルール、any を使うのをやめることです。
any ではなく unknown を使う
any は、その値とそこから計算されるすべてのものの型チェックを無効にします。unknown もどんな値でも受け付けますが、使う前にチェックしなければならないので、データがプログラムに入ってくる場所にチェックが置かれることになります:
境界とは、型が保証されなくなる場所のことです: JSON.parse、fetch のレスポンス、localStorage、フォームの入力、環境変数、ほかのプロセスからのメッセージ。そこで型ガードやスキーマライブラリを使って検証すれば、残りのコードは型を信頼できます。
strict を有効に保つ
TypeScript 7 では strict がデフォルトです。無効にしないでください。そして、strict に含まれないチェックのうち、バグをいちばん多く見つけるものを追加します:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
}
}
noUncheckedIndexedAccess は、arr[i] と record[key] の型に undefined を含めます。存在しないインデックスでは実際にそれが返るからです。noImplicitOverride はサブクラスのメソッドに override を書かせ、noFallthroughCasesInSwitch は次の case に流れ込む case を拒否します。
推論に任せる
型注釈を書くのは、TypeScriptが知りえないものです。関数のパラメーターと、ほかのモジュールが使う関数の戻り値の型です。ローカル変数とコールバックのパラメーターは推論に任せます。不要な型注釈はただの雑音ではなく、型を値より広くしてしまうこともあります:
@ts-expect-error のコメントがなければ、setStatus(annotated) はエラー TS2345 になります。推論された const はリテラル型 "active" を保つので受け付けられます。型を付ける前に、エディターで変数にカーソルを合わせて何が推論されたかを確認しましょう。
enum よりユニオン型を選ぶ
文字列リテラルのユニオンなら、生成されるコードなしで補完と網羅性チェックが得られます。実行時に値の一覧も必要なら、as const の配列から型を導きます:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
function canEdit(role: Role): boolean {
return role !== "viewer";
}
console.log(canEdit("editor")); // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]
canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
enum Role { Admin, Viewer } は逆マッピングを持つオブジェクトにコンパイルされ、数値 enum のパラメーターは enum にない値を持つものも含め、どんな number 変数でも受け付けてしまいます。また、enum は Node の型除去では動きません(TypeScript enum is not supported in strip-only mode)。それぞれの長所と短所は enum のページで比較しています。
設定オブジェクトは satisfies でチェックする
オブジェクトに Record<string, Route> のような広い型の型注釈を付けると、値はチェックされますがキーは失われます。satisfies なら同じチェックをしたうえで、正確な型が保たれます:
変数が宣言したとおりの型を持つべき場合(関数のパラメーター、再代入する値)は型注釈を使います。対応表、ルートの一覧、テーマのトークンなどの定数オブジェクトには satisfies を使います。
状態を判別可能なユニオンで表す
省略可能なフィールドを持つひとつのオブジェクトでは、ありえない状態を許してしまいます。loading: true と error が同時にある状態や、成功したのに data がない状態です。共通のタグを持つオブジェクトのユニオンなら本当にある状態だけが許され、各分岐には自分のフィールドだけが見えます:
never の行が網羅性チェックです。誰かが状態を追加して処理を忘れると、その行でビルドが壊れます:
エラーは index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. で、処理されていない case の名前を示しています。case "cancelled": を追加すればコンパイルが通ります。
! と as を避ける
非 null アサーション x! と型アサーション x as T は、コンパイラーにチェックをやめさせます。どちらも実行時の値を変えないので、間違ったアサーションは原因から遠く離れた場所で、あとになってクラッシュを起こします:
! は、?.、??、早期の return、わかりやすいメッセージ付きのエラーの送出に置き換えます。as は、値を実際にテストする型ガードに置き換えます。常に安全なアサーションは as const だけです。型を狭くして読み取り専用にするだけだからです。適切な lint の設定(typescript-eslint の no-non-null-assertion と no-explicit-any)が残りを指摘してくれます。
データを readonly にする
コードが変更すべきでないプロパティと配列には readonly を付けます。するとコンパイラーは push、sort、代入を拒否し、関数は入力を変更する代わりに新しい値を返すようになります:
readonly はコンパイル時にだけ、しかも1段階の深さでだけチェックされ、実行時にオブジェクトを凍結はしません。それでも、この種のバグの大半を引き起こす、共有された状態のうっかりした変更を見つけるには十分です。
よくある質問
TypeScriptで any を使うべきですか?
アプリケーションのコードではほぼ使いません。any はその値と、そこから導かれるすべてのチェックを無効にします。型がまだわからない値には unknown を使い、チェックで絞り込みます。any はまれな逃げ道として、理由を説明するコメントを付けて使いましょう。
TypeScriptではすべての変数に型注釈を書くべきですか?
いいえ。ローカル変数とコールバックのパラメーターは推論に任せます。型注釈を書くのは、推論できない関数のパラメーターと、エクスポートする関数の戻り値の型です。後者は、関数の中の変更で公開している型が黙って変わらないようにするためです。
TypeScriptの enum は悪い書き方ですか?
間違いではありませんが、避けるチームは多いです。enum は実行時のコードを生成し、Node の型除去では動かず、数値 enum のパラメーターはどんな値を持つ number 変数でも受け付けてしまいます。文字列リテラルのユニオン、または型を導く as const の配列なら、生成されるコードなしで同じ補完とチェックが得られます。
as による型アサーションはいつ使うべきですか?
コンパイラーにはわからないことを自分が知っているときだけで、できればそれを証明するチェックの直後にします。as は実行時の値を何も変えないので、{} as User はコンパイルが通っても name を持ちません。ほとんどの場合、値を実際にテストする型ガードのほうが安全です。
新しいTypeScriptのプロジェクトにはどの tsconfig 設定がよいですか?
strict を有効のまま(TypeScript 7 のデフォルト)にし、noUncheckedIndexedAccess を追加します。多くのプロジェクトは noImplicitOverride、noFallthroughCasesInSwitch、verbatimModuleSyntax も有効にしています。tsc --init が書き出す設定には、strict、noUncheckedIndexedAccess、exactOptionalPropertyTypes、verbatimModuleSyntax などが含まれます。