[TypeScript] querySelector/closestの返値の型

TypeScriptでは`querySelector`や`closest`に`'div'`や`'li'`のようなHTMLの要素名を指定すると、その返値の型は`HTMLDivElement`や`HTMLLIElement`のように要素名に応じた型となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122792/63dd5d68-8c79-f991-64d5-fffc40e7cfc1.png)

しかし`'div[data-selected]'`や`'li:last-child'`のような属性セレクタや擬似クラスなどが付いていると`Element`になってしまいます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122792/4ce20c9d-4fed-3dc4-514e-41a1dad15b1f.png)

もし`HTMLDivElement`型にしようとするなら型アサーションを付ける必要があります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122792/7a10bb7f-f1df-ba2c-a3bf-72431445ac06.png)

しかし

- 実際にはその要素が見つからないこともあるので` | null`を付ける必要がある
- 補完をミスって`HTMLDialogElement`にしてしまうかも知れない
- セレクタによって決まるものをわざわざ書くなんてめんどくさい(本音)

そこでTypeScriptの型関数でどうにかしてみました。

まずセレクタに指定された文字列から対応する要素の型に変換する型関数を用意します。

```ts
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`の返値をこの型に差し替えます。

```ts
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;
}
```

こうすることで属性セレクタや擬似クラスなどが付いているセレクタでも適切な型で返ってきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122792/7efd40f3-674f-057d-6110-9f3976df1c04.png)

兄弟結合子やセレクターリストにも対応しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122792/83b4a543-0324-eb51-c457-4b915026df88.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122792/423d720f-9e82-ce10-0c8d-a06174a8f8bd.png)

https://www.typescriptlang.org/ja/play/?target=99&jsx=0&ts=5.6.2#code/C4TwDgpgBAShDOBXANsAioiAnEBlCyEAxsAPZYA8A0lBAB7AQB2AJvFPMFgJZMDmAPigBeKDXqNW7AOQAqaQFgAUFCgB+KAAkAKgFkAMgFFCAW2bBlqgFxjaDZmygBrCCFIAzLXqOnz2gIZ8AHL+Zrr+YJbqXgbGEGZMwAHBoRDhYADaVAC6UTbi9lJQAAYAJADevO7YUABiAJIwuNoAvgA0FVU1MIbNLcVRGnBIqBjYeATEZJQNTdpCAD6wCCjomDj4hCTkFD3NAnm2Eg7sZeWcPPwtFdJQt0vSAvd3ANTP0gB+7wvS15VM1SwYio-UGyxGa3GmymOyoVAOKig+TskkcZy6QLhf2kADp3hl3gBid5WX4VC68PigxEaOEok4lclcSnYvFQB4E9l3Ylc6Skv4Uq4DRGqDRMCAAN2wUWswPpRRcbk8OlivkSyRCYQiMuiKp88T8gU1aQiWRyOpseriCQsiJs4qlWAA3MplLxGFh3P4iNAAAr+LDmIKkFjQcpRACO6wmW2m1HljkFggAFPBJtssPB8gBKGzDVZjDbpuPwrlMFDIF2IqNQ4vkACCyGQ8eORSTAmTOrTsfIWbEbSiuagwdD+m4nAA8u5ditRtHoRnqAIBFWWq6lO7sF6fVBreYoOHEURkKQ05wW4VE8z+B3uzCsDm87PIUWe5RS0ty03V+uiKQmJwUD+CIUAsKQRCIDaOI1q+97JtILDcBKGQsP4wD+AAtHeJAQCw2TSNmVZ-gBwBQAARiB-hqDix6nggwDwcg3BWMg-icBhRAABbcMgLAEVWyjuOQUDJsRgFEFAHigeBkHmNB851lgjbIPBXBtMALDqZxBHZgeOpED+ShiaRLAgWBEFQTBMZwdIrFkQQUAvFAvBgIgwD8coQA

ここではモジュールを使っていないのでそのままで使えていますが、モジュールを使う場合には`declare global {` ～ `}`で囲う必要がありますのでご注意を。

**2024/10/23 22:45追記**

書き忘れてました。

`:has`など引数にセレクタをとる擬似クラス関数を使っていると誤判定することがあります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/122792/25d609c4-6525-bb42-7408-fa7e59288a32.png)

といってもセレクタを引数にとる擬似クラス関数は、CSSでは使ってもスクリプト側で使うことはあまりないでしょう。(と信じたい)