querySelector の null チェック - throw すべきか、スキップすべきか

document.querySelector はジェネリクス引数を使うと、戻り値に null が残ります。

const form = document.querySelector<HTMLFormElement>("#login-form");
// 型: HTMLFormElement | null

この null をどう処理するかには、大きく 2 つのパターンがあります。throw して早期にエラーにするか、スキップして何もしないか。どちらを選ぶかは要素の性質によって決まります。

ないのがバグか、ないのが正常か

判断基準はシンプルで、「その要素が DOM に存在しないのはバグなのか、それとも正常な状態なのか」という一点です。

存在しないのがバグ

ページのレイアウトを構成する要素、常に表示されるフォーム、アプリの起動に必要なコンテナなど。これらが見つからないのは HTML の構造が壊れているか、セレクタが間違っている。

存在しないのが正常

条件付きで表示される通知、モーダル、ツールチップ、ログイン状態でのみ出るメニューなど。ユーザーの操作やサーバーの応答次第で DOM にないことがある。

この区別がそのまま、null の処理方法に対応します。

パターン 1: throw で早期エラー

ページに必ず存在するはずの要素には、取得直後の null チェックと throw を組み合わせます。

const form = document.querySelector<HTMLFormElement>("#login-form");
if (!form) throw new Error("#login-form not found");

const emailInput = document.querySelector<HTMLInputElement>("#email");
if (!emailInput) throw new Error("#email not found");

form.addEventListener("submit", (e) => {
  e.preventDefault();
  console.log(emailInput.value);
});

throw の後は型が HTMLFormElement に絞り込まれるため、以降のコードで null を気にする必要がなくなります。要素がない場合はエラーメッセージにセレクタ文字列が含まれるので、どの要素が見つからなかったのかがすぐにわかります。

このパターンを採用する理由は、問題の発見を早めるためです。null のまま処理が進むと、実際にエラーが起きるのは別の箇所になり、原因の特定に時間がかかります。取得直後に throw すれば「この要素がない」という事実がそのまま報告されます。

パターン 2: optional chaining でスキップ

存在しないことがあり得る要素には、optional chaining(?.)を使って「あれば処理、なければ何もしない」と書きます。

const toast = document.querySelector<HTMLDivElement>(".toast");
toast?.classList.add("visible");

const badge = document.querySelector<HTMLSpanElement>(".notification-badge");
badge?.textContent = unreadCount.toString();

要素がなければ式全体が undefined に評価され、何も起きません。エラーも投げません。条件付き UI のように「表示されていないときは何もしなくていい」場面では、これが最も簡潔な書き方です。

ただし optional chaining は処理が静かにスキップされるため、本来あるべき要素に対して使ってしまうとバグの発見が遅れます。ページに必須の要素に ?. を使うのは避けるべきです。

初期化関数にまとめる

ページの必須要素が複数ある場合、取得と検証を初期化関数にまとめるパターンがあります。

function initElements() {
  const form = document.querySelector<HTMLFormElement>("#login-form");
  if (!form) throw new Error("#login-form not found");

  const emailInput = document.querySelector<HTMLInputElement>("#email");
  if (!emailInput) throw new Error("#email not found");

  const passwordInput = document.querySelector<HTMLInputElement>("#password");
  if (!passwordInput) throw new Error("#password not found");

  return { form, emailInput, passwordInput };
}

const elements = initElements();

戻り値の型は自動的に null を含まない形に推論されるため、以降のコードは null チェックなしで各要素にアクセスできます。DOM 取得のロジックが一箇所に集まるので、HTML 側でセレクタが変わったときの修正箇所も明確になります。

この方法は SPA でない従来型のページ(Python フレームワークのテンプレートから生成された HTML を TypeScript で操作するケースなど)で特に有効で、エントリーポイントで必要な要素をすべて検証してから処理を開始する設計になります。

ページの JavaScript が最初に実行される起点となるファイルや関数。

まとめると

判断基準は 1 つだけです。その要素がないのはバグか、正常か。バグなら throw、正常なら ?. でスキップ。この使い分けを徹底するだけで、querySelector まわりの null 処理は一貫したものになります。