この記事は、SolidJSアドベントカレンダー 2024 1 日目の記事です。
実はv1.7.0の時にSolidJSの型のサポートが強化されてました!!
実はSolidJSの型のサポートが強化されてます。
release noteに書かれてるのと英語版ドキュメントにはそれが反映されてるのですが、日本語版ドキュメントには反映されてないので知らない人も多い思います。
かくゆう私も「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にそのままのっかって三項演算子による型ガードが行われます。
// 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を使うのですが関数になっているので型ガードが行われません。
// 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としてコールバックを渡すという方法で !
や ?
を回避する方法がとられていました。
// 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では次の書き方ができるように修正が行われました。
// 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を使うことができるようになりました
以下のように値だけ返すとuser()の値が変わってもdomに反映されないので注意(5時間混乱した…)
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として関数が許容されてました。
なので次の書き方ができます。
function Counter() {
const [count, setCount] = createSignal(1);
return <button onClick={() => setCount(c => c + 1)}>{count}</button>;
}
これは特に問題ないコードです。
ですが、次のコードも許容してしまいます。
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!
が表示されます。
上記のような非効率なコードを書けないようにするため、1つ目のコードの書き方すらできなくなるなどのトレードオフと引き換えにJSX Elementから関数が取り除かれました。
1つ目のコードのように書きたい場合は as unknown as JSX.Element
を使ってキャストするようにしてください。
あとがき
個人的にはTypeScriptを使いたいなら非効率な再レンダリングを許容しなきゃいけないと思っていたので、その懸念を払拭してくれた「Null-Asserted Control Flow」が結構衝撃でした。
この記事は「Null-Asserted Control Flow」を共有したいがために生まれた記事で、正直そこ以外の記述は「リリースノートに書いてあるし、一応書いとくか」くらいのものなのでスルーしてもいいです。(文字数稼ぎです)
「TypeScriptとの相性悪そうだしSolidJS使うのやめておこう」と思ってる方は、これからは是非Solidを使ってみてください。