Back

TypeScriptにおける`!`の使用に注意すべき理由

TypeScriptにおける`!`の使用に注意すべき理由

TypeScriptのnullセーフティ機能は、その最大の強みの一つです。strictNullChecksを有効にすると、コンパイラは値を使用する前に、その値がnullまたはundefinedである可能性を処理することを強制します。しかし、これらすべてをバイパスできる単一の文字があります:それが!です。

non-null assertion演算子は、コンパイラが警告を出したときに簡単に手を伸ばしてしまいがちです。この演算子が実際に何をするのか、そして何をしないのかを理解することで、追跡が非常に困難なバグから身を守ることができます。

重要なポイント

  • !(non-null assertion)演算子は、TypeScriptコンパイラに値をnon-nullとして扱うように指示しますが、実行時チェックは一切生成されません — 出力されるJavaScriptでは完全に削除されます。
  • !の過度な使用は、コンパイル時エラーを実行時クラッシュに変換することで、strictNullChecksの目的を損ないます。
  • 明示的なnullチェック、オプショナルチェイニング(?.)、nullish coalescing(??)、カスタムアサーションガードなどのより安全な代替手段を最初の選択肢とすべきです。
  • !は、コンパイラが検証できない真に外部的な知識がある場合にのみ使用し、すべての使用箇所をコードレビューのフラグとして扱うべきです。

TypeScriptの!演算子が実際に行うこと

式に!を付加すると、TypeScriptコンパイラに「信じてください、この値はnullでもundefinedでもありません」と伝えることになります。コンパイラは型からnullundefinedを削除し、警告を出さなくなります。

// strictNullChecksが有効な場合
const input = document.querySelector('input') // 型: HTMLInputElement | null
const value = input!.value // 型: string — コンパイラは満足

ここで重要なのは:!は出力されるJavaScriptで完全に削除されるということです。実行時チェックはありません。ガードもありません。セーフティネットもありません。実行時にinputが実際にnullであれば、最初に回避しようとしていたのと同じCannot read properties of nullエラーが発生します。

Definite Assignment AssertionとNon-Null Assertionの違い

!演算子は2つの異なるコンテキストで使用され、それらを区別する価値があります。

Non-null assertion — 式に使用して、型からnull | undefinedを取り除きます:

const el = document.getElementById('app')! // HTMLElement、HTMLElement | nullではない

Definite assignment assertion — クラスフィールドや変数宣言で使用し、プロパティが使用前に割り当てられることをコンパイラに伝えます(コンパイラがそれを検証できない場合でも):

class UserService {
  user!: User // "これは読み取られる前に割り当てられることを約束します"
}

どちらも、実行時保護を追加することなくコンパイラを黙らせるコンパイル時アサーションです。non-null assertionは式の型からnull | undefinedを削除し、definite assignment assertionは変数やクラスフィールドの確定代入チェックを無効にします。

!の過度な使用がTypeScriptのNullセーフティを損なう理由

!演算子は脱出ハッチであり、解決策ではありません。これを使用すると、nullability問題を修正しているのではなく、コンパイラから隠しながら実行時には完全にそのまま残しているのです。

実際にバグを引き起こす一般的なパターン:

// 危険: 要素が常に存在すると仮定
const button = document.querySelector('.submit-btn')!
button.addEventListener('click', handleSubmit)

特定のコンテキスト(異なるページ、条件付きレンダリング、テスト環境など)でその要素が存在しない場合、実行時クラッシュが発生します。コンパイラは警告を出しませんでした。なぜなら、あなたが出さないように指示したからです。

最初に使用すべきより安全な代替手段

現代のTypeScriptには強力な制御フロー解析があります。開発者が歴史的に!に手を伸ばしていた多くの状況で、コンパイラは単純なガードで型を自動的に絞り込むことができます。

明示的なnullチェック:

const button = document.querySelector('.submit-btn')
if (button) {
  button.addEventListener('click', handleSubmit)
}

オプショナルチェイニングとnullish coalescingの組み合わせ:

const label = document.querySelector('label')?.textContent ?? 'Default'

型ガード関数:

function assertExists<T>(
  val: T | null | undefined,
  msg: string
): asserts val is T {
  if (val == null) throw new Error(msg)
}

const el = document.getElementById('app')
assertExists(el, 'App element not found')
el.style.display = 'block' // HTMLElementに絞り込まれる

アサーションガードアプローチは特に有用です。なぜなら、安全性の保証を維持するからです — 値がnullの場合、下流のどこかで不可解なクラッシュが発生するのではなく、失敗箇所で明示的で説明的なエラーが発生します。

!が実際に適切な場合

正当な使用例は存在します。コンパイラが推論できない外部知識がある場合 — たとえば、TypeScriptが追跡できない初期化ライフサイクルによって保証される値など — !は合理的なツールです。重要な質問は:コンパイラが知らない何かを実際に知っているのか、それとも単に警告を黙らせているだけなのか?

後者の場合、その警告はおそらく正しいのです。

結論

TypeScriptの!演算子は、コードをより安全にするのではなく、より静かにします。不注意に使用すると、コンパイル時エラーを実行時クラッシュに変換してしまい、これはstrictNullChecksが防ぐように設計されていることの正反対です。制御フロー絞り込み、オプショナルチェイニング、または明示的なガードを最初に使用してください。!は、コンパイラが検証できない真の確実性がある場合にのみ使用し、すべての使用箇所を検討に値するコードレビューフラグとして扱ってください。

よくある質問

いいえ。感嘆符はコンパイル中に完全に削除されます。出力されるJavaScriptには、nullチェックやガードは一切含まれません。実行時に値がnullまたはundefinedであることが判明した場合、TypeScriptを全く使用していなかった場合とまったく同じようにコードはエラーをスローします。

危険な場合があります。user!: Userのようなプロパティを記述すると、コードが読み取る前に値が割り当てられることをコンパイラに伝えています。その仮定が間違っている場合 — たとえば、期待される初期化ステップがスキップされた場合 — プロパティにアクセスするとundefinedが返され、コンパイル時の警告なしに実行時エラーが発生する可能性があります。

コンパイラが検証できない外部知識がある場合に合理的です。一般的な例は、TypeScriptが追跡できない初期化ライフサイクルによって値が保証されている場合です(たとえば、フレームワークの初期化フック、依存性注入など)。その場合でも、明示的なアサーションガードの使用を検討してください。そうすれば、値が欠落している場合に不可解なクラッシュではなく、明確なエラーメッセージが生成されます。

最良の代替手段はコンテキストによって異なります。DOMルックアップやオプショナルなデータの場合は、オプショナルチェイニングとnullish coalescingを使用してください。値が欠落していることが真のエラーである場合は、説明的なメッセージをスローするカスタムアサーションガード関数を使用してください。どちらのアプローチも、実際の実行時安全性を追加しながら型の絞り込みを維持します。

Complete picture for complete understanding

Capture every clue your frontend is leaving so you can instantly get to the root cause of any issue with OpenReplay — the open-source session replay tool for developers. Self-host it in minutes, and have complete control over your customer data.

Check our GitHub repo and join the thousands of developers in our community.

OpenReplay