Skip to content

svelte2tsx: $bindable() with a renamed prop key emits a use-reference under the wrong name #3016

@Hoffs

Description

@Hoffs

Describe the bug

When a $bindable() prop is destructured with a renamed key (source: renamed), svelte2tsx emits a "mark prop as used" reference using the source key rather than the renamed identifier in scope. The destructure binds to the renamed name, so the emitted reference points at an identifier that doesn't exist.

This is the workaround introduced for #2268 — emit ;propName; so the prop counts as used. The issue is that the workaround uses the wrong name when there's a rename.

Severity scales with the source key:

Key Emitted reference tsc/tsgo
Non-reserved (foo, x) ;foo; TS2304 "Cannot find name" — normal semantic error, isolated
Reserved word (class, default, for) ;class; TS1005 syntax errortsc -p/tsgo -p then silently drops semantic diagnostics across the whole program

svelte-check --tsgo and svelte-check --incremental route through the overlay tsconfig + tsc -p, so a single component using class: name = $bindable() can hide every TS2339-class error in the project. Default svelte-check uses a per-file diagnostic path and isn't affected.

Reproduction

import { svelte2tsx } from 'svelte2tsx';
import * as compiler from 'svelte/compiler';

const cases = [
  ['rename + $bindable() (reserved key)',     '<script lang="ts">let { class: className = $bindable() }: { class?: string } = $props();</script>'],
  ['rename + $bindable() (non-reserved key)', '<script lang="ts">let { x: y = $bindable() }: { x?: string } = $props();</script>'],
  ['rename only (no $bindable)',              '<script lang="ts">let { class: className }: { class?: string } = $props();</script>'],
  ['$bindable() only (no rename)',            '<script lang="ts">let { foo = $bindable() }: { foo?: string } = $props();</script>'],
];
for (const [name, src] of cases) {
  const out = svelte2tsx(src, { parse: compiler.parse, version: compiler.VERSION, filename: 'x.svelte', isTsFile: true, mode: 'ts' });
  console.log('---', name, '---\n', out.code);
}

The reserved-key case emits ;class; and the non-reserved case emits ;x;. Running tsc --noEmit confirms TS1005 and TS2304 respectively. Removing $bindable() removes the use-emit entirely; removing the rename makes the use-emit reference the same name as the binding (correct).

Expected behaviour

The use-reference should match the in-scope identifier — i.e. the renamed name when present, otherwise the key:

// for `class: className = $bindable()`
let { class: className = $bindable() }: $$ComponentProps = $props();
className;   // correct, in scope, no syntax issue

System Info

  • OS: macOS 14 (arm64)
  • Editor: Neovim (irrelevant — the bug is in svelte2tsx codegen, surfaces in any tsserver/tsc/tsgo)
  • `svelte`: 5.55.2
  • `svelte2tsx`: 0.7.53
  • `svelte-check`: 4.4.8
  • `typescript`: 6.0.2
  • `@typescript/native-preview`: 7.0.0-dev (latest)

Which package is the issue about?

svelte2tsx (root cause); svelte-check (amplifies via diagnostic suppression in --tsgo/--incremental)

Additional Information

Probably a one-line fix in the $bindable()-rewriting path — wherever the trailing ;${key}; is appended, use the renamed identifier when the destructure has key: name form, falling back to key when there's no rename.

Related: #2268 (introduced the ;propName; workaround).

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions