📎

[svelte5] union型の $state を $derived で参照すると型エラーになることがある

2024/12/02に公開

バグだと思って報告もしたのですが結果として svelte のバグではなかったし自分の理解も浅かったので調べてまとめました。

バージョン情報

svelte@5.2.8
typescript@5.6.3
svelte-check@4.1.0

エラーになる例

<script lang="ts">
  type Person = { name: string }

  let p: Person | undefined = $state(undefined)

  $effect(() => {
    // load person
    setTimeout(() => {
      p = { name: "bob" }
    }, 1000);
  });

  let displayName = $derived(p?.name ?? "anon")
</script>
<div>{ displayname }</div>

このコンポーネントに svelte-check で型チェックを実行すると、次のエラーが起きます。

Error: Property 'name' does not exist on type 'never'. (ts)

> let displayName = $derived(p?.name ?? "anon")

修正方法

先に解決方法から書くと、現状としては以下のどちらかの方法で修正できます。解説は下へ。

  1. $state の型引数を明示する

    - let p: Person | undefined = $state(undefined)
    + let p = $state<Person | undefined>(undefined)
    
  2. $derived.by を使う

    - let displayName = $derived(p?.name ?? "anon")
    + let displayName = $derived.by(() => p?.name ?? "anon")
    

解説

型推論の話になるので順番に説明すると以下のようになります。

  1. $state の型はジェネリクス関数として宣言されている [1]

    declare function $state<T>(initial: T): T;
    
  2. 最初のエラーになる例では $state の型引数 T が明示されていないため、TypeScript が型推論をおこなう。

  3. 型推論にて、 initial の型が undefined なため 型引数T は undefined と推論され、それにより戻り値の型も undefined となる。

  4. 変数 p は型注釈があるため Person | undefined 型だが、代入による narrowing により、そのスコープ内では undefined 型になる。 [2]

    // 代入による narrowing の例
    let x: number | string = "foo"; // 文字列を代入
    console.log(x.length);  // narrowing によりこの時点の `x` の型は
                            // `string` 型となり、 `.length` を参照できる
    x = 3.1;     // 数値を代入
    x.toFixed(3) // 同様に narrowing により `number` 型となり、 toFixed を呼び出せる
    
  5. undefined?. のオプショナルチェーンは常に実行されないため、 narrowing により pnever 型になる

    // narrowing により never 型になる例
    const v = true;
    if (v) {
      console.log(v);  // v: true
    } else {
      console.log(v);  // v: never
    }
    
  6. never はプロパティ参照できないので 型エラーとなる

derived.by について

$derived.by の場合、与えた関数を呼び出した時点で p の値が変更されているかどうかは不明なため(言い換えると代入による narrowing の適応範囲外なため) Person | undefined 型として扱われ、p?. も有効なコードになる

今後について

自分が挙げた issue としてはこれになりまして(英語がクソ雑魚なのはご容赦ください)、「型引数を明示しなくても state を記述できるほうがシンプルじゃない?」みたいなことを言っていますが、それで型定義が必要以上に複雑になるのもどうか、という思いもあります。また $derived の構文が原因で narrowing が発生するのも微妙なので $derived.by を使った方がいいと言う感もあり、どうなるかは今後の議論次第という感じがします。ともあれ、現状としては型引数を明示するのがよさそうです。

参考

脚注
  1. 実際は ルーンはコンパイルされる? ↩︎

  2. narrowing: その位置で最も具体的な値の型を分析し、宣言された型からその部分型に絞り込む過程。typeof での分岐以外で行われることもあり、その一つに代入がある ↩︎

Discussion