Search code examples
typescriptsveltesvelte-5

$state run in svelte 5 is narrowing type


I have a filter variable of type "all" | "active" | "completed", which I initialize to 'all'

(in fact it's reactive, declared with $state('all') but I can reproduce the issue with a plain variable

so this works OK:

  let filter: "all" | "active" | "completed" = "all"; // same thing happens if I use $state("all")

  function filteredFn() {
    console.log({ filter }); // => (property) filter: "all" | "active" | "completed"
    if (filter === "active") return todos.filter((t) => !t.completed); // OK!
    if (filter === "completed") return todos.filter((t) => t.completed); //OK!
    return todos;
  }

but if I try to do the same in side a $derived.by() rune, filter is assumed to be of type 'all' as if it were declared with as const, like this:

  let filter: "all" | "active" | "completed" = "all";

  let filtered = $derived.by(() => {
    console.log({ filter }); // => (property) filter: "all" !!!! WRONG!!!

    if (filter === "active") return todos.filter((t) => !t.completed);
    // This comparison appears to be unintentional because the types '"all"' and '"active"' have no overlap.ts(2367)
    if (filter === "completed") return todos.filter((t) => t.completed);
    // This comparison appears to be unintentional because the types '"all"' and '"completed"' have no overlap.ts(2367)
    return todos;
  });

I can fix it with:

let filter = "all" as "all" | "active" | "completed";

or with the $state rune:

let filter = $state<"all" | "active" | "completed">("all");

but I don't know why it doesn't work the same way it works with regular functions

It this a bug or am I missing something???


Solution

  • The problem should be control flow analysis.

    If the variable is not changed anywhere, its type is narrowed to just that single value. This happens more often with a regular $derived since that does not even have a function scope, so more aggressive analysis is possible.

    As soon as you e.g. have a binding to filter or set it somewhere in the <script> the issue should disappear.

    TS Playground demo