How to disambiguate svelte components?

48 views Asked by At

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.

1

There are 1 answers

0
Jacob Runge On

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}