TypeScriptの厳密モード: 何が有効になるのか

1. 概要

"strict": true は tsconfig.json の中で最も重要な行です。しかし多くの人がそれを有効にしていながら、実際に何が有効になるのかを知りません。この記事では、strict が有効にする各フラグを、各フラグが実際に検出するエラーとともに説明し、strict の外に有効にすべきいくつかのフラグも紹介します。最後に、既存プロジェクトを厳密モードに移行する方法があり、数千のエラーを一度に修正する必要はありません。

2. strict は複数のフラグのグループ

strict は単一のチェックではありません。複数のフラグを一度に有効にするマスタースイッチです:

{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "strict": true
  }
}

TypeScript 5.x と 6.x では、strict: true は以下と同等です:

{
  "compilerOptions": {
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "strictBuiltinIteratorReturn": true,
    "noImplicitThis": true,
    "useUnknownInCatchVariables": true,
    "alwaysStrict": true
  }
}

覚えておくべき2つのポイントがあります:

  • TypeScript が新しい「厳密な」チェックを追加するたびに(例えば、4.4 の useUnknownInCatchVariables や 5.6 の strictBuiltinIteratorReturn)、そのチェックは自動的に strict で有効になります。そのため、TypeScript をアップグレードするとコードを触らなくても新しいエラーが出ることがあります。
  • strict を有効にした後、個別のフラグを無効にすることができます。具体的な設定は常にグループ設定を上書きします。

TypeScript 6.0 は strict のデフォルト値を true に変更しました。しかし、明示的に tsconfig.json で指定することをお勧めします。そうすることで、設定を読む人がいちいち推測する必要がありません。

以下の例を試すには、TypeScript をインストールして tsc --noEmit を実行するだけです:

npm install --save-dev typescript
npx tsc --noEmit

3. 最も重要なフラグ

3.1. noImplicitAny

TypeScript が型を推論できない場合、暗黙的に any を割り当てます。このフラグはこのケースをエラーに変えます。

function calculateTotal(items) {
  return items.reduce((sum, item) => sum + item.price, 0);
}
error TS7006: Parameter 'items' implicitly has an 'any' type.

このフラグなしでは、items は any です。その場合、item.prise(タイプミス)は正常にコンパイルされます。修正方法は型を明示的に宣言することです:

interface OrderItem {
  name: string;
  price: number;
}

function calculateTotal(items: OrderItem[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

このフラグは暗黙的な any のみをブロックします。本当に必要な場合は明示的に any を書くことは常にできます。

3.2. strictNullChecks

これは最も価値のあるフラグです。無効な場合、null と undefined は任何の型に割り当てることができます。有効な場合、それらは独立した型になり、明示的に処理する必要があります。

interface Customer {
  id: number;
  email: string;
}

const customers: Customer[] = [{ id: 1, email: "an@example.com" }];

function getCustomerEmail(id: number): string {
  const customer = customers.find((c) => c.id === id);
  return customer.email;
}
error TS18048: 'customer' is possibly 'undefined'.

Array.prototype.find は Customer | undefined を返します。フラグを有効にしない場合、TypeScript は undefined を無視し、Cannot read properties of undefined エラーはランタイムまで見つかりません。修正方法:

function getCustomerEmail(id: number): string | undefined {
  const customer = customers.find((c) => c.id === id);
  return customer?.email;
}

関数は現在 string | undefined を返します。戻り値の型は事実を反映しているため、この関数を呼ぶ側は見つからないケースも処理する必要があります。

注意: non-null アサーション customer!.email でエラーを「修正」しないでください。! オペレーターはコンパイラーを黙らせるだけで、何もチェックしません。値が確実に存在し、コンパイラーが見ることができない理由がある場合のみ使用してください。

3.3. strictPropertyInitialization

このフラグは、クラスのすべてのプロパティが値を割り当てられることを要求します。宣言時、またはコンストラクター内でです。strictNullChecks が有効な場合のみ効果があります。

class PaymentService {
  apiKey: string;
  retries = 3;

  constructor(private readonly baseUrl: string) {}
}
error TS2564: Property 'apiKey' has no initializer and is not definitely assigned in the constructor.

3つの方法があります:

class PaymentService {
  apiKey: string;              // 1. コンストラクター内で割り当て
  webhookSecret?: string;      // 2. オプショナルとして宣言
  client!: HttpClient;         // 3. 確定的割り当て: "信じて、割り当てられる"

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }
}

interface HttpClient {
  post(url: string, body: unknown): Promise<unknown>;
}

方法3(!)はしばしば Dependency Injection またはデコレーターを使うフレームワークで見られます。例えば Angular や TypeORM です。そこではプロパティはコンストラクター実行後に割り当てられます。そのようなケース以外では、方法1または2を優先してください。

3.4. useUnknownInCatchVariables

JavaScript では任何のものを throw することができ、必ずしも Error である必要がありません。このフラグは catch ブロック内の変数の型を any から unknown に変えます。

async function chargeOrder(orderId: string): Promise<void> {
  try {
    await fetch(`/api/orders/${orderId}/charge`, { method: "POST" });
  } catch (err) {
    console.error(err.message);
  }
}
error TS18046: 'err' is of type 'unknown'.

err を使用するには、最初に型を絞り込む必要があります:

catch (err) {
  const message = err instanceof Error ? err.message : String(err);
  console.error(`チャージに失敗しました: ${message}`);
}

3.5. strictFunctionTypes

このフラグは関数型のパラメーターを反変的に検査します。簡単に言うと: より狭い型のみを処理できる関数をより広い型を処理する必要があるプレイスに渡すことはできません。

interface Animal { name: string; }
interface Dog extends Animal { bark(): void; }

const handleDog = (dog: Dog) => dog.bark();

const handleAnimal: (animal: Animal) => void = handleDog;
error TS2322: Type '(dog: Dog) => void' is not assignable to type '(animal: Animal) => void'.

上記の割り当てが受け入れられた場合、handleAnimal({ name: "ネコ" }) を呼ぶと、渡されたオブジェクトに bark がないためクラッシュします。1つ知るべき例外があります: このフラグはインターフェース内で method(x: T): void 構文で宣言されたメソッドには適用されません。これは既存の API との互換性を保つためのTypeScriptの意図的な選択です。

3.6. 残り3つのフラグ

これら3つのフラグは新しいコードではあまりエラーを起こしませんが、知る価値があります:

function formatPrice(amount: number, currency: string): string {
  return `${amount.toFixed(2)} ${currency}`;
}

formatPrice.call(null, "100", "VND"); // strictBindCallApply

function onClick() {
  console.log(this.id);               // noImplicitThis
}
error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
error TS2683: 'this' implicitly has type 'any' because it does not have a type annotation.
  • strictBindCallApply は call、apply、bind を呼ぶ際のパラメーターをチェックします。
  • noImplicitThis は this が暗黙的に any 型のときエラーを報告します。修正するには function onClick(this: HTMLButtonElement) のように this パラメーターを宣言します。
  • alwaysStrict は各出力ファイルに "use strict" を発行し、コードを JavaScript の厳密モードでパースします。ES modules では厳密モードはすでにデフォルトです。

4. strict が有効にしないフラグ

多くの人は strict: true が最も厳格だと思っています。そうではありません。以下のフラグは strict グループの外にありますが、新しいプロジェクトでは非常に価値があります:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true
  }
}

この中で、noUncheckedIndexedAccess が最も重要です。デフォルトでは、TypeScript はインデックスによる配列アクセスまたは動的キーによるオブジェクトアクセスが常に値を持つと見なします:

const stockBySku: Record<string, number> = { "SKU-1": 10 };

const quantity = stockBySku["SKU-999"];
console.log(quantity.toFixed(0));

このフラグなしでは、quantity は number 型であり、上記のコードはランタイムで失敗します。フラグを有効にすると、quantity は number | undefined 型になります:

error TS18048: 'quantity' is possibly 'undefined'.

残りのフラグ:

  • exactOptionalPropertyTypes: discount?: number は「キーが存在しないかもしれない」という意味であり、discount: undefined を明示的に割り当てることはできません。
  • noImplicitOverride: 親クラスのメソッドをオーバーライドするとき、override キーワードを必須にします。これにより、親クラスがメソッド名を変更すると、子クラスはエラーを報告し、新しいメソッドが作成されることはありません。
  • noImplicitReturns: 関数のすべてのブランチで return する必要があります。
  • noFallthroughCasesInSwitch: switch で break を忘れるケースをブロックします。

注意: noUncheckedIndexedAccess はかなり「ノイズが多い」です。ループ for (let i = 0; i < arr.length; i++) でも arr[i] は T | undefined 型になります。for...of または arr.forEach に切り替えてください。大きなプロジェクトではこのフラグは最後に有効にしましょう。

5. テストで動作をチェック

型チェックはコンパイル時に実行されますが、ランタイム動作と戻り値の型の両方に対してテストを持つべきです。Vitest は両方をサポートします:

npm install --save-dev vitest
// customer.ts
export interface Customer { id: number; email: string; }

const customers: Customer[] = [{ id: 1, email: "an@example.com" }];

export function getCustomerEmail(id: number): string | undefined {
  return customers.find((c) => c.id === id)?.email;
}
// customer.test.ts
import { describe, it, expect, expectTypeOf } from "vitest";
import { getCustomerEmail } from "./customer";

describe("getCustomerEmail", () => {
  it("顧客が見つかったときメールを返す", () => {
    expect(getCustomerEmail(1)).toBe("an@example.com");
  });

  it("見つからないときは undefined を返す", () => {
    expect(getCustomerEmail(42)).toBeUndefined();
  });

  it("戻り値の型に undefined が含まれる", () => {
    expectTypeOf(getCustomerEmail).returns.toEqualTypeOf<string | undefined>();
  });
});

3番目のテストは型テストです。後で誰かが関数シグネーチャーを : string に変更して ! を使ったとしましょう。その場合、vitest --typecheck(またはテストファイルで tsc --noEmit)がエラーを報告します。なぜなら戻り値の型が string | undefined と一致しなくなるからです。

npx vitest run --typecheck
 ✓ customer.test.ts (3 tests)
 Test Files  1 passed (1)
      Tests  3 passed (3)

6. 既存プロジェクトを厳密モードに移行する

大きなコードベースで strict: true を有効にすると、数千のエラーが発生する可能性があります。実用的なアプローチは段階的に行うことです。

ステップ1: strict を有効にしますが、最も「ノイズが多い」フラグを一時的に無効にします。

{
  "compilerOptions": {
    "strict": true,
    "strictNullChecks": false,
    "strictPropertyInitialization": false,
    "useUnknownInCatchVariables": false
  }
}

noImplicitAny は通常最も修正が簡単なので、最初に処理します。フラグを修正し終わったら、そのフラグを無効にする行を削除します。新しく書かれたコードは自動的に有効なすべてのチェックを受けます。

ステップ2: フラグごとのエラー数をカウントして、作業量を見積もります。

npx tsc --noEmit --strictNullChecks true | grep -c "error TS"

ステップ3: strictNullChecks では、ディレクトリごとに修正します。 まだ修正していない部分は // @ts-expect-error で一時的にマークします。理由を付けたコメントを付けます。@ts-ignore は使わないでください。理由: @ts-expect-error は下の行にエラーがなくなるとエラーを報告するため、余ったコメントを見落としません。

// @ts-expect-error TODO(strict): customer は undefined の可能性があり、PR #482 で処理
const email: string = findCustomer(id).email;

ステップ4: CI で回帰をブロックします。 パイプラインに tsc --noEmit を追加して、新しい型エラーを起こすコードがマージされないようにします。

注意: skipLibCheck や any を乱用して「厳密モードを急速に有効化」しないでください。skipLibCheck: true は .d.ts ファイルのチェックのみをスキップするため、保持できます。しかし、至る所に any をばら撒くと、移行のすべての利点が失われます。

7. 結論

strict: true は9つのチェックを有効にします。最も大きな価値をもたらす2つのチェックは noImplicitAny と strictNullChecks です。これらは undefined is not a function と Cannot read properties of undefined のほとんどのエラーをコンパイル時に防ぎます。新しいプロジェクトでは、strict を有効にしてから noUncheckedIndexedAccess、noImplicitOverride、exactOptionalPropertyTypes も有効にすべきです。既存プロジェクトでは、strict を有効にしてから個別のフラグを一時的に無効にし、難易度順に段階的に修正し、CI を使ってエラー数が減少だけするようにしましょう。

typescripttsconfigstrict modetype safety