TypeScriptではquerySelectorやclosestに'div'や'li'のようなHTMLの要素名を指定すると、その返値の型はHTMLDivElementやHTMLLIElementのように要素名に応じた型となります。
しかし'div[data-selected]'や'li:last-child'のような属性セレクタや擬似クラスなどが付いているとElementになってしまいます。
もしHTMLDivElement型にしようとするなら型アサーションを付ける必要があります。
しかし
- 実際にはその要素が見つからないこともあるので| nullを付ける必要がある
- 補完をミスってHTMLDialogElementにしてしまうかも知れない
- セレクタによって決まるものをわざわざ書くなんてめんどくさい(本音)
そこでTypeScriptの型関数でどうにかしてみました。
まずセレクタに指定された文字列から対応する要素の型に変換する型関数を用意します。
type ResultQuerySelector<K extends string> = K extends '*'
  ? HTMLElement
  : K extends keyof HTMLElementTagNameMap
  ? HTMLElementTagNameMap[K]
  : K extends `${infer FIRST},${infer REST}`
  ? ResultQuerySelector<FIRST> | ResultQuerySelector<REST>
  : K extends `${string}${' ' | '>' | '+' | '~' | '|'}${infer KK}`
  ? ResultQuerySelector<KK>
  : K extends `${infer KK}${'.' | '[' | '#' | ':'}${string}`
  ? KK extends `${string}${'.' | '[' | '#' | ':'}${string}`
    ? never
    : KK extends keyof HTMLElementTagNameMap
    ? HTMLElementTagNameMap[KK]
    : HTMLElement
  : never;
querySelectorの定義にはSVGやMathMLのものもありますが、とりあえずHTMLElement派生のものだけに限定しています。必要なら上記のHTMLElementTagNameMapを置き換えてみてください。
次にquerySelector(ついでにquerySelectorAllも)やclosestの返値をこの型に差し替えます。
interface ParentNode {
  querySelector<K extends string>(selectors: K): ResultQuerySelector<K> | null;
  querySelectorAll<K extends string>(
    selectors: K,
  ): NodeListOf<ResultQuerySelector<K>>;
}
interface Element {
  closest<K extends string>(selector: K): ResultQuerySelector<K> | null;
}
こうすることで属性セレクタや擬似クラスなどが付いているセレクタでも適切な型で返ってきます。
兄弟結合子やセレクターリストにも対応しています。
ここではモジュールを使っていないのでそのままで使えていますが、モジュールを使う場合にはdeclare global { ~ }で囲う必要がありますのでご注意を。
2024/10/23 22:45追記
書き忘れてました。
:hasなど引数にセレクタをとる擬似クラス関数を使っていると誤判定することがあります。
といってもセレクタを引数にとる擬似クラス関数は、CSSでは使ってもスクリプト側で使うことはあまりないでしょう。(と信じたい)






