実はsolidjsの型のサポートが強化されてる話


この記事は、[SolidJSアドベントカレンダー 2024](https://qiita.com/advent-calendar/2024/solidjs) 1 日目の記事です。

## 実はv1.7.0の時にSolidJSの型のサポートが強化されてました！！

実はSolidJSの型のサポートが強化されてます。

[release note](https://github.com/solidjs/solid/releases/tag/v1.7.0)に書かれてるのと[英語版ドキュメント](https://github.com/solidjs/solid-docs-legacy/commit/87a648f71c895e51922247bbbc930481f3fce6c0)にはそれが反映されてるのですが、[日本語版ドキュメント](https://www.solidjs.com/guides/typescript#%E5%88%B6%E5%BE%A1%E3%83%95%E3%83%AD%E3%83%BC%E3%81%AE%E7%B5%9E%E3%82%8A%E8%BE%BC%E3%81%BF)には反映されてないので知らない人も多い思います。

かくゆう私も「TypeScriptのサポート弱いし業務じゃ使えないな」と思ってました。

ですが、よくよく調べてみたら全然TypeScriptが使えるようになっていたので、今回はその紹介をしたいと思います。

## 何が変わったの？

- **Null-Asserted Control Flow**
    - nullableな値をShowに渡して分岐するときにより安全にコードが欠ける&効率的に実行されるようになった
- **Better Event Types for Input Elements**
    - onChageなどのtargetがより正確な型を持つようになった
- **Stricter JSX Elements**
    - childrenとして関数を渡せるようにしていたが、効率の悪いコードが欠ける余地を作ってしまうため関数を渡せないようにした

## 具体的に

### **Null-Asserted Control Flow**

Reactでは三項演算子などを使ってDOM出力するものを制御するので、TypeScriptにそのままのっかって三項演算子による型ガードが行われます。

```tsx
// Reactだったらこんな感じで書くはず
type User = {name: string}

function Comp(props: {user: User | null}){
	const [user, setUser] = useState<User|undefined>();
  return <div>
    {user? <div>{user.name}</div> : null}
  </div>
 }
```

一方solidではreactivityにsignalを使うのですが関数になっているので型ガードが行われません。

```tsx
// Solidだとこうなる
type User = {name: string}

function Comp(){
	const [user, setUser] = createSignal<User|undefined>();
  return <div>
	  <Show when={user()}>
	    {/* whenでのuser()と↓のuser()は異なる可能性があるため型ガードができない */}
	    <div>{user()!.name}</div>
      </Show>
  </div>
 }
```

なのでこういう時はShowのkeyedとchildrenとしてコールバックを渡すという方法で `!` や `?` を回避する方法がとられていました。

```tsx
// Solidで型ガードが欲しいならこう
type User = {name: string}

function Comp(){
	const [user, setUser] = createSignal<User|undefined>();
  return <div>
    {/* keyedを渡す */}
	  <Show when={user()} keyed>
	    {/* uの返値がuser()のNonNullable版、user()が変わるたびに再レンダリングされて非効率 */}
	    {(u)=><div>{u.name}</div>}
	  </Show>
  </div>
 }
```

ですがその方法だとShowに渡す値が変わるたびにコールバックが再実行されて再レンダリングが走ってしまっていました。

そこでv1.7.0では次の書き方ができるように修正が行われました。

```tsx
// v1.7.0以降はこう！！！！
type User = {name: string}

function Comp(){
	const [user, setUser] = createSignal<User|undefined>();
  return <div>
    {/* keyedがいらない*/}
	  <Show when={user()}>
	    {/* uが関数になった。user()が変わったら変更は反映されるが、再レンダリングはされない */}
	    {(u)=><div>{u().name}</div>}
	  </Show>
  </div>
 }
```

これによって再レンダリングを気にせず、また型も `!` や `?` で無理することなくShowを使うことができるようになりました

:::note alert
以下のように値だけ返すとuser()の値が変わってもdomに反映されないので注意(5時間混乱した…)

```tsx
function Comp(){
	const [user, setUser] = createSignal<User|undefined>();
  return <div>
    {/* keyedがいらない*/}
	  <Show when={user()}>
	    {/* ↓なぜかuser()の値が変わっても反映されない、Fragmanet(<></>)で囲ってあげれば問題ない */}
	    {(u)=>u().name}}
	  </Show>
  </div>
 }
```
:::

### **Better Event Types for Input Elements**

EventにおけるtargetとcurrentTargetは次の違いがあります。

| プロパティ名 | 説明 |
| --- | --- |
| Event.currentTarget | イベントハンドラを登録した要素 |
| Event.target | イベントが発生した要素 |

なので、これまではEvent.targetの型はElementが採用されてましたが、reactを習って `HTMLInputElement`, `HTMLTextAreaElement`, `HTMLSelectElement`  の`onInput`, `onChange`, `onBlur`, `onFocus`, `onFocusIn`, `onFocusOut` ではEvent.targetにそれら要素の型を採用するようになりました 。

これによって慣れ親しんだ `event.target.value` や `event.target.checked` が使えます。

### **Stricter JSX Elements**

これまではJSX Elemtentsとして関数が許容されてました。

なので次の書き方ができます。

```tsx
function Counter() {
  const [count, setCount] = createSignal(1);

  return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
```

これは特に問題ないコードです。

ですが、次のコードも許容してしまいます。

```tsx
function MyComp(props) {
  return () => {
    console.log("rendered!");
    if (props.count > 5) {
      return <div>Maximum Tries</div>;
    }

    return <div>Attempt {props.count}</div>;
  };
}
```

上記のコードはprops.countが変更されるたびに再レンダリングされ `rendered!` が表示されます。

上記のような非効率なコードを書けないようにするため、１つ目のコードの書き方すらできなくなるなどのトレードオフと引き換えにJSX Elementから関数が取り除かれました。

１つ目のコードのように書きたい場合は `as unknown as JSX.Element` を使ってキャストするようにしてください。

## あとがき

個人的にはTypeScriptを使いたいなら非効率な再レンダリングを許容しなきゃいけないと思っていたので、その懸念を払拭してくれた「Null-Asserted Control Flow」が結構衝撃でした。

この記事は「Null-Asserted Control Flow」を共有したいがために生まれた記事で、正直そこ以外の記述は「リリースノートに書いてあるし、一応書いとくか」くらいのものなのでスルーしてもいいです。（文字数稼ぎです）

「TypeScriptとの相性悪そうだしSolidJS使うのやめておこう」と思ってる方は、これからは是非Solidを使ってみてください。