sveltejs / svelte

Cybernetically enhanced web apps
https://svelte.dev
MIT License
76.8k stars 3.98k forks source link

Svelte 5: ComponentProps does not correctly resolve conditional property types #11485

Closed Serator closed 1 week ago

Serator commented 1 week ago

Describe the bug

Hi there. I am trying to create a component on Svelte 5 with conditional types, where property a is only possible if it has property b.

+page.svelte:

<script lang="ts">
  import RedButton from './RedButton.svelte'
</script>

<RedButton a="-" b="-" />

RedButton.svelte:

<script lang="ts">
  import Button from './Button.svelte'
  import type {ComponentProps} from 'svelte'

  let {a, b}: ComponentProps<Button> = $props()
</script>

<!-- Error: Type 'undefined' is not assignable to type 'string'. -->
<Button {a} {b} class="bg-red-400" />

<!-- Error: Type 'undefined' is not assignable to type 'string'. -->
<Button {...a ? {a, b} : {}} class="bg-red-400" />

<!-- Error: Type 'undefined' is not assignable to type 'string'. -->
<Button {...false ? {a, b} : {}} class="bg-red-400" />

Button.svelte:

<script lang="ts">
  let {class: className}: {class?: string} & (
    | {
      a: string
      b?: string
    }
    | {
      a?: never
      b?: never
    }
  ) = $props()
</script>

<button class={className}></button>

How can I pass properties to RedButton.svelte typing them with ComponentProps<Button> and not get type errors? Thanks!

Reproduction

https://stackblitz.com/edit/vitejs-vite-fzujoz?file=src%2Froutes%2FRedButton.svelte

Logs

Loading svelte-check in workspace: /home/projects/vitejs-vite-fzujoz
Getting Svelte diagnostics...

/home/projects/vitejs-vite-fzujoz/src/routes/RedButton.svelte:9:10
Error: Type 'string | undefined' is not assignable to type 'string'.
  Type 'undefined' is not assignable to type 'string'. (ts)
<!-- Error: Type 'string | undefined' is not assignable to type 'string'. -->
<Button {a} {b} class="bg-red-400" />

/home/projects/vitejs-vite-fzujoz/src/routes/RedButton.svelte:12:7
Error: Type '{ class: string; a?: string | undefined; b?: string | undefined; }' is not assignable to type '$$ComponentProps | undefined'.
  Type '{ class: string; a?: string | undefined; b?: string | undefined; }' is not assignable to type '{ class?: string | undefined; } & { a: string; b?: string | undefined; }'.
    Type '{ class: string; a?: string | undefined; b?: string | undefined; }' is not assignable to type '{ a: string; b?: string | undefined; }'.
      Types of property 'a' are incompatible.
        Type 'string | undefined' is not assignable to type 'string'.
          Type 'undefined' is not assignable to type 'string'. (ts)
<!-- Type 'undefined' is not assignable to type 'string'. -->
<Button {...a ? {a, b} : {}} class="bg-red-400" />

/home/projects/vitejs-vite-fzujoz/src/routes/RedButton.svelte:15:7
Error: Type '{ class: string; a?: string | undefined; b?: string | undefined; }' is not assignable to type '$$ComponentProps | undefined'.
  Type '{ class: string; a?: string | undefined; b?: string | undefined; }' is not assignable to type '{ class?: string | undefined; } & { a: string; b?: string | undefined; }'.
    Type '{ class: string; a?: string | undefined; b?: string | undefined; }' is not assignable to type '{ a: string; b?: string | undefined; }'.
      Types of property 'a' are incompatible.
        Type 'string | undefined' is not assignable to type 'string'.
          Type 'undefined' is not assignable to type 'string'. (ts)
<!-- Type 'undefined' is not assignable to type 'string' -->
<Button {...false ? {a, b} : {}} class="bg-red-400" />

====================================
svelte-check found 3 errors and 0 warnings in 1 file

System Info

"svelte": "^5.0.0-next.123",
"svelte-check": "^3.7.1",

Severity

annoyance

brunnerh commented 1 week ago

By separating the properties, the relationship provided by the type is broken. If you don't split, it should work as intended:

<script lang="ts">
  import Button from './Button.svelte'
  import type { ComponentProps } from 'svelte'

  let { ...props }: ComponentProps<Button> = $props()
</script>

<Button {...props} class="bg-red-400" />
Serator commented 1 week ago

Hmmm, this is not the pattern I use to describe properties as I prefer a more explicit way, but it works. Thanks! šŸ‘šŸ»

Could you please clarify for this example from the code above?

<Button {...false ? {a, b} : {}} class="bg-red-400" />

Here I explicitly pass "nothing", but TS still outputs an error.

7nik commented 1 week ago

It's more about TS itself. When you destruct a and b, their types get squashed. Example.

Serator commented 1 week ago

Oh, it all makes sense now. Thanks! šŸ‘šŸ»