2

I have the following typescript type:

type ItemTypes = (GameItem & { id: undefined }) | (CurrencyItem & { id: string });

The GameItem is an object that doesn't have the "id" attribute so normally I would disambiguate like this in TS:

if (item.id !== undefined) {
  // CurrencyItem
} else {
  // GameItem
}

However in my svelte component if I try to do the equivalent TS complains that the value can be the other type:

{#if item.id !== undefined}
  <!-- CurrencyItem expects item to be `CurrencyItem` so I get a "Type 'GameItem' is not assignable to type 'CurrencyItem'." -->
  <CurrencyItem item={item} />
{:else}
  <!-- GameItem expects item to be `GameItem` so I get a "Type 'CurrencyItem' is not assignable to type 'GameItem'." -->
  <GameItem item={item} />
{/if}

Is there a way to avoid the error? Thanks.

6
  • {#if item.id !== undefined} <CurrencyItem item={item as CurrencyItem} /> {:else} <GameItem item={item as GameItem} /> {/if} Commented Oct 19, 2023 at 12:31
  • 1
    this should work, are you on the latest version of everything ? Commented Oct 19, 2023 at 13:24
  • I don't think the Svelte tooling carries TS narrowing to the {#if} blocks, but I could be wrong. I still have that feeling, though. Can anyone test? Cannot test right now. Commented Oct 19, 2023 at 16:01
  • 1
    @JoséRamírez: It does type narrowing and I have tested this.
    – brunnerh
    Commented Oct 19, 2023 at 16:21
  • 1
    typescript inside templates (like the as) is not supported Commented Oct 20, 2023 at 10:51

1 Answer 1

0

This is the one area where I've not been in love with Svelte -- it enforces type checking, but doesn't allow you to use any actual TypeScript syntax within the markup. The result is that I tend to do type manipulation in the <script> tag. I would do something like this:

In <script>:

function asCurrency(item: GameItem | CurrencyItem): CurrencyItem | null {
  if(item.id === undefined) return item as CurrencyItem;
  return null;
}

function asGame(item: GameItem | CurrencyItem): GameItem | null {
  if(item.id !== undefined) return item as GameItem;
  return null;
}

And in your markup:

{#if asCurrency(item)}
  <CurrencyItem item={asCurrency(item)} />
{:else if asGame(item)}
  <GameItem item={asGame(item)} />
{/if}

Admittedly, this feels SUPER hacky, but the general principle of keeping your logic out of your markup is sound, even if it is inconvenient. In most cases, this has the effect of keeping your code easier to read and maintain; I always end up regretting inlining functions and evaluations in my markup, even though I'm tempted to do so every darn time. In this case, though, since what we're really after is satisfying the TypeScript linter, it's just ugly.


EDIT: If you don't want to call each function twice, you can abuse {#await}:

{#await asCurrency(item) then currencyItem}
  {#if item}
    <CurrencyItem item={currencyItem} />
  {:else}
    <GameItem item={asGame(item)} />
  {/if}
{/await}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.