Hacker News new | ask | show | jobs
by teekoiv 462 days ago
I very much agree with the overall thesis of the post. Runes are just irksome in subtle ways.

One big point that the post misses, is that the Class escape hatch for runes is incompatible with constructor-set parameters.

Say you have a class that wraps a HTMLElement which you set in the constructor. This doesn't work:

  class Wrapper {
    dom: HTMLElement = $state()
    constructor(el: HTMLElement) {
      this.dom = el
    }
  }
as TypeScript throws an error about `Type 'undefined' is not assignable to type 'HTMLElement' for the $state()`. You could fix it by eg. `$state(undefined as unknown as HTMLElement)` but that's dumb. Interesting enough you could do something like:

  class Wrapper {
    dom: HTMLElement
    constructor(el: HTMLElement) {
      let d = $state(el)
      this.dom = d
    }
  }
Moreover, Vite/esbuild mangles class-field parameters with esnext into constructor-set parameters as they are just more versatile. So the original code becomes something like:

  class Wrapper {
    constructor(el: HTMLElement) {
      this.dom = $state(el)
    }
  }
Which is incompatible with rules of runes. I did whine about this already https://github.com/sveltejs/svelte/issues/14600 but so far no clear answer
3 comments

I agree with everything in the post but I still like using Svelte. We've adopted our own Store class and decorators which eliminates some of the issues in the post.

    class Wrapper extends Store {
        @state() accessor dom: HTMLElement;
        constructor(el: HTMLElement) { this.dom = el; }
    }
The advantages here are that a) we don't need the .svelte.ts postfix and b) @state() makes TS support flawless with runes. And since we only use classes for state, most of the other objections in the post are mitigated. On the bindable issue, we simply just don't use that feature - one way dataflow ftw! :)

I'm not saying Svelte 5 is flawless at all, but I think we found an approach that minimizes the downsides. The upside is really good performance and a metaframework that makes the most sense out of all the current options.

Oh wow, so this still works even though Svelte doesn't support TypeScript features that aren't just type annotations (like decorators)? Is that because you're outside of `svelte.ts` files so the compiler never touches them?
Yeah it works fine with the latest sveltekit. You do need to set { esbuild: target: "es2022" } in the vite config to enable JS decorators but that isn't specific to svelte. These aren't TS decorators, they're ecmascript decorators (https://github.com/tc39/proposal-decorators) which are at stage 3 and being adopted by JS runtimes.

Under the hood the class is just creating a hidden property with $state({}) and the accessors are reading/writing to that property. Since $state() creates a deeply reactive object it acts the same as marking individual fields with $state().

> You could fix it by eg. `$state(undefined as unknown as HTMLElement)` but that's dumb

I think the generic is what you are looking for

  class Wrapper {
    dom = $state<HTMLElement>();
    constructor(el: HTMLElement) {
      this.dom = el;
    }
  }
the dom property will still be HTMLElement | undefined, if the 'undefined' bothers you have to add an exclamation mark and write "$state<HTMLElement>()!"
That's still a manual type assertion though, and if a regular usage pattern demands one, then the library is doing it wrong. Regardless of how you annotate it, every manual override reduces the effectiveness of the type system.
Use $state()! to fix the undefined is not assignable error.