querySelector に型引数を指定しない

必要もないのに querySelectorquerySelectorAll の型引数を指定しないようにしましょう.

(この記事は AI レビュワーに「型引数を指定した方が型安全だ」と提案されたのに対する反論として作成しています.)

querySelector の型安全性

querySelectorquerySelectorAll の型定義は, 後述する要素型セレクターに関連する部分を除くと, 基本的には以下のようになっています.

querySelector<E extends Element = Element>(selectors: string): E | null;

querySelectorAll<E extends Element = Element>(selectors: string): NodeListOf<E>;

これはつまり型引数が Element を継承していれば何であっても受け入れて, その型の値を返すという意味です. 戻り値の型を任意に決められてしまうということは, こんなものには as と同程度の安全性しかありません.

// as を使った場合, 型が間違っていてもエラーにならない
const element = document.querySelector("a.foo") as HTMLButtonElement | null;
// element: HTMLButtonElement | null

// 型引数を使った場合も, 型が間違っていてもエラーにならない
const element = document.querySelector<HTMLButtonElement>("a.foo");
// element: HTMLButtonElement | null

as を使うと以下のようにより誤った記述 (| null を忘れている) をしてしまう可能性があるので型引数を指定した方が良いという主張 (参考) は間違ってはいませんが, これは as と比べてミスしづらいというだけであって, 両者がともに型安全でないことには変わりありません.

// null の可能性を無視していてより危険!
const element = document.querySelector("a.foo") as HTMLButtonElement;
// element: HTMLButtonElement

我々が唯一常に信用できるのは, 戻り値の型が少なくとも Element | null であるということだけです. そしてその範囲で使用する際には型引数の指定は必要ありません.

// セレクターがなんであれ戻り値の型は常に正しく型安全
const element = document.querySelector("a.foo");
// element: Element | null

ケーススタディ

セレクターが要素型セレクターのみの場合

セレクターが要素型セレクター (要素名) のみの場合は型定義に特別扱いがあり, 何もしなくても要素固有の型が推論されるようになっているので, 型引数を指定する必要はありません.

const element = document.querySelector("a");
// element: HTMLAnchorElement | null

むしろ指定しようとすると誤った型を指定してしまう可能性があるので, 指定しないようにしましょう. 意外にも以下はエラーになりません.

const element = document.querySelector<HTMLButtonElement>("a");
// element: HTMLButtonElement | null

セレクターから要素の型が明らかな場合

要素型セレクターが含まれるなどセレクターから要素の型が明らかに読み取れる場合であっても, 複合的なセレクターになっていたら要素固有の型は推論されません.

const element = document.querySelector("a.foo");
// element: Element | null

もし要素固有の型が必要であればランタイムで型検査をするか, 型安全性を諦めて型引数を指定することになります. この場合は型引数とセレクターがすぐ近くにあってその一致を確認しやすいため, 多くの場合は型引数を指定することが許容されるでしょう.

const element = document.querySelector<HTMLAnchroElement>("a.foo");
// element: HTMLAnchroElement | null

ただし一致を確認するのは型システムではなく人間なので, セレクターを変更する際には必ず合わせて型引数も変更するようにしましょう. あるいは人間でなくても linter で機械的に検査することもできるかもしれません.

また要素固有の型が必要でなく Element で十分であれば, そもそも型引数を指定する必要はありません. 指定しなくて済むなら指定しない方がミスが入り込む余地がないため安全です.

その他の理由で要素の型がほぼ明らかな場合

プロジェクト内の常識や周辺コードの知識などによって, 要素の型がほぼ明らかな場合もあります.

例えば Element を継承している型には HTMLElement だけでなく SVGElementMathMLElement もありますが, 常識的に HTML の要素しか登場しない場面であれば HTMLElement と決めつけてしまっても安全性は大きく損なわれません.

const element = document.querySelector<HTMLElement>(".foo");
// element: HTMLElement | null

また要素が同じファイルに記述されている場合のように, 見ての通りその型であって概ね同時に変更されることが期待される場合も, 型引数を指定してしまっても問題はないでしょう.

const element = document.querySelector<HTMLAnchorElement>(".foo");
// element: HTMLAnchorElement | null

とはいえやはりこの場合も, より詳細な型を必要とせず Element で十分であれば, 型引数を指定しないでいる方が断然良いです.

要素の型が明らかでない場合

Element よりも具体的な型が必要であればランタイムで型をチェックしましょう.

const element = document.querySelector(".foo");
// element: Element | null
if (element instanceof HTMLAnchorElement) {
  // element: HTMLAnchorElement
  console.log(element.href);
}

まとめ

  • querySelectorquerySelectorAll に型引数を指定するのは as と同様であり, 型安全とは言えない
    • 誤って null を無視してしまいづらいという点で, as よりは型引数を指定する方がマシではあるが
  • Element よりも具体的な型が必要, かつ上記のように許容される場合に限って型引数を指定すること