1. ホーム
  2. TypeScript

【TypeScript】tsconfig.json の strict オプションを解説|noImplicitAny・strictNullChecks の役割

Share

TypeScript のプロジェクトを作ると、tsconfig.json"strict": true という1行がほぼ必ず入っています。これは単独のチェックではなく、複数の厳格な型チェックをまとめて有効にするスイッチです。この記事では、strict によって何がオンになるのかを一覧で整理したうえで、代表的な noImplicitAnystrictNullChecksstrictPropertyInitialization がどんなコードをエラーにするのかをコード付きで解説します。あわせて、個別オプションだけをオフにする書き方と、既存プロジェクトに後から導入するときの現実的な進め方も紹介します。

strict は複数のチェックをまとめて有効にするフラグ

strict は、compilerOptions に書く真偽値のオプションです。true にすると、型安全性を高めるための個別オプションが一括で有効になります。逆に strict を書かない(既定値の false)場合、それらの個別オプションはすべて無効のままになり、型を書き忘れても、null が入り得る値をそのまま使っても、コンパイラは何も言いません。

tsconfig.json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true
  },
  "include": ["src"]
}

重要なのは、strict が「まとめ役」でしかないという点です。エラーメッセージに出てくるのは strict ではなく、実際にそのエラーを出した個別オプション(たとえば strictNullChecks)の挙動です。エラーの意味を理解するには、まとめられている中身を知っておく必要があります。

strict で有効になるオプション一覧

strict が有効にする主なオプションは次のとおりです。TypeScript のバージョンが上がると新しいチェックがこのファミリーに追加されることがあるため、バージョンを上げた直後に見慣れないエラーが出た場合は、公式ドキュメントの strict の項目を確認すると原因が見つかります。

オプション役割
noImplicitAny型注釈がなく型も推論できない箇所(関数の引数など)が暗黙の any になることをエラーにする
strictNullChecksnullundefined を独立した型として扱い、他の型に勝手に代入できないようにする
strictFunctionTypes関数型どうしの代入可能性を、引数の型について厳しく(反変で)チェックする
strictBindCallApplybind / call / apply に渡す引数の型と個数をチェックする
strictPropertyInitializationクラスのプロパティが宣言だけされて初期化されていない状態をエラーにする(strictNullChecks が必要)
noImplicitThisthis の型が推論できず暗黙の any になる箇所をエラーにする
alwaysStrictファイルを ECMAScript の strict mode として解析し、出力に "use strict" を付ける
useUnknownInCatchVariablescatch で受け取る変数の型を any ではなく unknown にする
strictBuiltinIteratorReturn組み込みイテレーターの TReturnany ではなく undefined として型付けする(TypeScript 5.6 以降)

名前が似ていて紛らわしいのですが、noUnusedLocals(未使用の変数)や noImplicitOverrideexactOptionalPropertyTypes などは strict には含まれません。これらを使いたい場合は、個別に true と書く必要があります。

noImplicitAny:型の書き忘れを見逃さない

もっとも遭遇しやすいのが noImplicitAny です。関数の引数に型注釈を書かないと、TypeScript はその型を推論できず「暗黙的に any」として扱います。any になった値はどんな操作をしても型チェックが通らなくなるため、そこから先の型安全性が丸ごと失われます。noImplicitAny は、この暗黙の any が発生した時点でエラーにしてくれます。

tax.ts
// エラー: パラメーター 'price' は暗黙的に 'any' 型になります。(ts7006)
function addTax(price) {
  return price * 1.1;
}

// price に型を書けば解消する
function addTaxSafe(price: number): number {
  return price * 1.1;
}

// 呼び出し側のミスも検出できるようになる
addTaxSafe('1000'); // エラー: 型 'string' の引数を型 'number' のパラメーターに割り当てられません。

注意したいのは、型注釈がない=すべてエラー、ではないことです。const price = 1000; のように初期値から型が推論できる箇所は問題ありません。エラーになるのは「推論の手がかりがなく any にせざるを得ない」ケースだけです。コールバック関数のように、渡し先のシグネチャから型が決まる引数(items.map((item) => item.id)item など)も推論が効くので書かなくて構いません。

どうしても型が決まらない値を受け取るときは、any と書いて逃げるより unknown を使うのがおすすめです。unknown は「何でも入るが、使う前に必ず型を確かめる必要がある型」なので、型チェックの網を残したまま受け取れます。

strictNullChecks:null と undefined を別の型として扱う

strictNullChecks が無効のとき、nullundefined はほぼすべての型に代入できてしまいます。string 型と書いた変数に null が入り、実行時に「Cannot read properties of null」で落ちる、という典型的な事故がこれです。有効にすると nullundefined が独立した型になり、それらが入り得る値は string | null のようにユニオン型で明示しなければならなくなります。

user.ts
type User = { id: number; name: string };

const users: User[] = [
  { id: 1, name: 'yamada' },
  { id: 2, name: 'suzuki' },
];

let nickname: string = 'たくみ';
// nickname = null; // エラー: 型 'null' を型 'string' に割り当てることはできません。(ts2322)

// null を許すなら型で明示する
let optionalNickname: string | null = null;

// find は見つからない可能性があるので User | undefined を返す
const user = users.find((u) => u.id === 3);

// console.log(user.name); // エラー: 'user' は 'undefined' の可能性があります。(ts18048)

Array.prototype.finddocument.getElementById のように「見つからないかもしれない」API の戻り値に undefinednull が含まれるようになるため、有効化した直後はこの種のエラーが一気に増えます。ただしこれは、これまで見えていなかった実行時クラッシュの候補が可視化されただけです。対処は難しくなく、値を使う前に絞り込むだけです。

narrowing.ts
const user = users.find((u) => u.id === 3); // User | undefined

// 1. 型ガードで絞り込む(このブロック内では User 型になる)
if (user) {
  console.log(user.name);
}

// 2. 早期リターンで、以降を User 型として扱う
function printName(target: User | undefined): void {
  if (!target) return;
  console.log(target.name);
}

// 3. オプショナルチェーンで安全に参照する(結果は string | undefined)
console.log(user?.name);

// 4. null 合体演算子で既定値を与える(結果は string)
const displayName = user?.name ?? 'ゲスト';

// 5. どうしても確実だと分かっている場合のみ非 null アサーション
const el = document.getElementById('app')!; // HTMLElement

最後の !(非 null アサーション)は「絶対に null ではない」とコンパイラに宣言するだけで、実行時のチェックは一切行われません。間違っていればそのまま実行時エラーになるので、多用すると strictNullChecks を入れた意味が薄れます。基本は 1〜4 の方法で絞り込み、! は最後の手段と考えてください。

strictPropertyInitialization:クラスのプロパティ初期化を必須にする

クラスのプロパティを baseUrl: string; と宣言したのに、コンストラクターで代入していない場合、そのプロパティは実行時には undefined です。strictPropertyInitialization はこれをエラーとして検出します。nullundefined を型として区別できることが前提なので、このオプションは strictNullChecks が有効でないと指定できません。

api-client.ts
class ApiClient {
  // エラー: プロパティ 'baseUrl' に初期化子がなく、
  // コンストラクターで明確に割り当てられていません。(ts2564)
  baseUrl: string;

  // 1. 宣言時に初期値を与える
  timeout: number = 5000;

  // 2. コンストラクターで必ず代入する
  token: string;

  // 3. 未設定があり得るなら型に undefined を含める
  userAgent: string | undefined;

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

コンストラクターの引数に publicprivate といった修飾子を付けると、代入とプロパティ宣言を同時に行えるため、この種のエラーをまとめて避けられます。また、DI コンテナやテストのセットアップ関数など、コンストラクターの外から確実に代入されると分かっている場合に限り、baseUrl!: string;! を付けて「明確な代入アサーション」を使えます。こちらも実行時の保証はないので、乱用は避けましょう。

api-client-fixed.ts
class ApiClient {
  // 引数に修飾子を付けると、宣言と代入を同時に行える
  constructor(
    private readonly baseUrl: string,
    private readonly token: string,
    private readonly timeout: number = 5000,
  ) {}

  buildUrl(path: string): string {
    return `${this.baseUrl}${path}`;
  }
}

const client = new ApiClient('https://example.com/api', 'xxxx');
console.log(client.buildUrl('/users'));

catch と this まわりで増えるエラー

残りのオプションの中で、既存コードに手を入れる必要が出やすいのが useUnknownInCatchVariablesnoImplicitThis です。前者は catch の変数を unknown にします。JavaScript では throw で投げられる値は Error とは限らないため、これは実態に合った型付けです。e.message と書きたい場合は、先に Error かどうかを確かめます。

parse.ts
function parseConfig(text: string): unknown {
  try {
    return JSON.parse(text);
  } catch (e) {
    // e は unknown なので、e.message とは書けない
    if (e instanceof Error) {
      console.error(`設定の読み込みに失敗しました: ${e.message}`);
    } else {
      console.error('設定の読み込みに失敗しました', e);
    }
    return null;
  }
}

noImplicitThis は、this の型が推論できない関数をエラーにします。オブジェクトのメソッドやクラスの中では this が推論できるので問題になりませんが、コールバックとして渡した通常の関数の中で this を使うと引っかかります。多くの場合はアロー関数に書き換えれば解決し、どうしても必要なら第1引数に this の型を宣言する構文で明示できます。

個別のオプションだけをオフにする

strict はあくまで既定値をまとめて切り替えるものなので、個別オプションを明示的に書けばそちらが優先されます。JSON 内での記述順は関係ありません。「基本は厳格にしたいが、このチェックだけは今は無理」というときは、次のように書きます。

tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    // strict で有効になるが、このチェックだけ無効にする
    "strictPropertyInitialization": false,
    "useUnknownInCatchVariables": false
  }
}

逆に、strictfalse(または未指定)のままにして、対応できるチェックから1つずつ有効にしていくこともできます。既存プロジェクトに後から導入するときは、こちらの向きのほうが進めやすいことが多いです。

tsconfig.json
{
  "compilerOptions": {
    // まずは暗黙の any の撲滅から始める
    "noImplicitAny": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

なお、TypeScript の設定ファイルは JSON5 に近い形式で、上の例のようにコメントを書けます。tsconfig.json でのコメントは仕様として認められているので、なぜそのオプションを切っているのかを1行残しておくと、後から戻すときの判断材料になります。

有効にしたら数百件のエラーが出たときの進め方

ある程度の規模のプロジェクトで "strict": true をいきなり入れると、エラーが数百件から数千件になることは珍しくありません。ここで諦めて設定を戻してしまいがちですが、順番を工夫すれば地道に減らせます。

まずエラーの内訳を数える

件数の合計だけを見ると絶望的に感じますが、実際にはエラーコードごとに偏っています。tsc を型チェック専用モード(--noEmit)で走らせ、エラーコード別に集計すると、どのオプションが原因なのかが一目で分かります。ts7006 が多ければ noImplicitAnyts18048ts2322 が多ければ strictNullChecks が主因です。

ターミナル
# 型チェックだけ実行する
npx tsc --noEmit

# エラーコードごとに件数を集計して、多い順に並べる
npx tsc --noEmit | grep -oE 'error TS[0-9]+' | sort | uniq -c | sort -rn

影響の小さいオプションから1つずつ有効にする

集計結果をもとに、件数の少ないオプションから順に true にしていきます。alwaysStrictnoImplicitThisstrictBindCallApply は影響範囲が狭いことが多く、先に片付けやすい部類です。逆に strictNullChecks は影響が最も大きいので、他が終わってから腰を据えて取り組みます。1オプションずつ有効化してその都度コミットしておくと、レビューもしやすくなります。

新しく書くコードだけ先に厳格にする

既存コードの修正が終わるのを待たずに、新規ファイルだけ厳格にする方法もあります。厳格な設定を継承した別の設定ファイルを用意し、対応済みのディレクトリだけを include して CI で型チェックを回す構成です。少なくとも新しく書いたコードは strict の恩恵を受けられ、対応が済んだファイルを include に足していけば、じわじわと範囲を広げられます。

tsconfig.strict.json
{
  // 既存の設定を引き継いだうえで、strict だけ上書きする
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "strict": true,
    "noEmit": true
  },
  // strict 対応が済んだ範囲だけを対象にする
  "include": ["src/features/billing", "src/lib/date"]
}

その場しのぎには @ts-expect-error を使う

どうしてもすぐに直せない箇所は、コメントで一時的にエラーを抑制できます。このとき @ts-ignore ではなく @ts-expect-error を使うのがおすすめです。@ts-expect-error は「次の行にエラーがあるはず」という宣言なので、後でコードを直してエラーが消えると、今度はコメント自体が不要だとエラーで教えてくれます。抑制コメントの消し忘れを防げるうえ、grep で残作業の一覧を作れます。

やってはいけないのは、エラーを消すために any を撒いたり、! を機械的に付けて回ったりすることです。それでは strict を有効にしたコストだけ払って、得られるはずの安全性が手に入りません。型が分からない値は unknown で受けて型ガードで絞り込む、というのが基本の型付けだと覚えておいてください。

まとめ

tsconfig.json"strict": true は、noImplicitAnystrictNullChecks をはじめとする複数の厳格チェックをまとめて有効にするフラグです。noImplicitAny は型を推論できない箇所が暗黙の any になるのを防ぎ、strictNullChecksnullundefined を独立した型として扱うことで実行時のクラッシュ候補を型エラーとして可視化します。strictPropertyInitialization はクラスのプロパティの初期化漏れを検出します。個別オプションを明示すれば strict より優先されるため、「基本は厳格、この1つだけオフ」も、「無効のまま1つずつ有効化」も自由に組み立てられます。既存プロジェクトではエラーコードごとに内訳を数え、影響の小さいものから順に有効にしていくのが現実的です。新規プロジェクトなら、最初から strict を有効にしておくのがもっとも手間の少ない選択になります。

参考ページ