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では使ってもスクリプト側で使うことはあまりないでしょう。(と信じたい)