Recipe · Frameworks and tests

How do I type Svelte component props in JavaScript?

  • Browser (no bundler)
  • Svelte
  • svelte-check
  • $props
  • @type
  • Checked with TypeScript 5.9

The problem My Svelte components use plain JavaScript in their script blocks. How do I type a component's props with JSDoc so that svelte-check reports a missing or wrong prop where the component is used?

The short answer

In the component’s <script>, put a @type on the $props() destructuring, the JSDoc form the Svelte documentation shows for JavaScript components:

<script>
  import { cartTotal, formatCents } from "./cart.js";

  /** @import { CartLine } from "./cart.js" */

  /** @type {{ lines: CartLine[], currency?: string, oncheckout: (total: number) => void }} */
  let { lines, currency = "USD", oncheckout } = $props();

  let total = $derived(cartTotal(lines));
</script>

<ul>
  {#each lines as line (line.sku)}
    <li>{line.name} × {line.quantity}</li>
  {/each}
</ul>
<button onclick={() => oncheckout(total)}>Pay {formatCents(total, currency)}</button>

The types the component shares live in a plain JavaScript module, which tsc checks on its own:

cart.js cart.ts TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @typedef {object} CartLine
 * @property {string} sku
 * @property {string} name
 * @property {number} cents
 * @property {number} quantity
 */

/**
 * @param {CartLine[]} lines
 * @returns {number}
 */
export function cartTotal(lines) {
  return lines.reduce((total, line) => total + line.cents * line.quantity, 0);
}

/**
 * @param {number} cents
 * @param {string} [currency]
 * @returns {string}
 */
export function formatCents(cents, currency = "USD") {
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
}
export interface CartLine {
  sku: string;
  name: string;
  cents: number;
  quantity: number;
}

export function cartTotal(lines: CartLine[]): number {
  return lines.reduce((total, line) => total + line.cents * line.quantity, 0);
}

export function formatCents(cents: number, currency = "USD"): string {
  return new Intl.NumberFormat("en-US", { style: "currency", currency }).format(cents / 100);
}
The module the component imports, with the type it shares.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

Why it works

Svelte 5 components receive their props through $props(), and a @type on that destructuring declares them. svelte-check turns each component into code TypeScript’s checker can read, so the JSDoc in a JavaScript <script> is checked like JSDoc in a .js file, using your jsconfig.json (with checkJs on). Uses of <Cart … /> in other components are then checked against the declared props.

How this page was checked. The component above and a parent that uses it were run through svelte-check 4.7.7 (Svelte 5.57.2, TypeScript 5.9.3) in a one-off run on 21 October 2026, with checkJs, strict, module: "esnext" and moduleResolution: "bundler". This site’s build does not run svelte-check; the .js module examples are checked on every build.

The same thing in TypeScript

The TypeScript equivalent tab shows the module as cart.ts. A component written in TypeScript uses <script lang="ts"> and the same props type as an annotation: let { lines }: Props = $props();.

What goes wrong

In that one-off run, this parent component left out lines on one <Cart> and treated the oncheckout total as a string on another:

<script>
  import Cart from "./Cart.svelte";

  /** @type {import("./cart.js").CartLine[]} */
  let lines = $state([{ sku: "LAMP-01", name: "Desk lamp", cents: 3900, quantity: 1 }]);
</script>

<Cart {lines} currency="EUR" oncheckout={(total) => console.log(total.toFixed(0))} />
<Cart {lines} oncheckout={(total) => total.trim()} />
<Cart oncheckout={() => {}} />

svelte-check reported both, and nothing for the first, correct use:

src/App.svelte:9:44  Error: Property 'trim' does not exist on type 'number'.
src/App.svelte:10:2  Error: Property 'lines' is missing in type '{ oncheckout: () => void; }' but required in type '$$ComponentProps'.

The JavaScript module is checked by tsc directly, so a caller that builds a cart line without quantity fails in the .js file itself (TS2741):

checkout.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: checkout.js(4,24): error TS2741: Property 'quantity' is missing in type '{ sku: string; name: string; cents: number; }' but required in type 'CartLine'. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. checkout.js(4,24): error TS2741: Property 'quantity' is missing in type '{ sku: string; name: string; cents: number; }' but required in type 'CartLine'. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. checkout.js(4,24): error TS2741: Property 'quantity' is missing in type '{ sku: string; name: string; cents: number; }' but required in type 'CartLine'. checked 21 October 2026
import { cartTotal } from "./cart.js";

console.log(cartTotal([{ sku: "LAMP-01", name: "Desk lamp", cents: 3900 }]));error TS2741: Property 'quantity' is missing in type '{ sku: string; name: string; cents: number; }' but required in type 'CartLine'.
The same module called with lines that are missing a field.
jsconfig.json for this example
{
  "compilerOptions": {
    "checkJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "nodenext",
    "lib": ["es2022", "dom", "dom.iterable"],
    "types": []
  },
  "include": ["*.js"]
}

Gotchas

  • List .svelte files in include. svelte-check reads the include list of jsconfig.json; in the one-off run, leaving src/**/*.svelte out produced only a warning that no Svelte files were found, and no component was checked. tsc itself ignores those entries.
  • Svelte 4 used export let. In Svelte 4, props were export let declarations, typed with a @type above each one; the $props() form is Svelte 5.
  • Events are props in Svelte 5. A callback prop such as oncheckout is typed like any function prop, which is what makes the second error above possible.

Questions

Does tsc check .svelte files?

No. tsc reads .js and .ts files only; svelte-check is the tool that understands .svelte files and runs TypeScript's checker over their scripts, including JSDoc in JavaScript ones. Run both: tsc (or svelte-check alone) for the .js modules, svelte-check for the components.

Where do I put types that several components share?

In a plain .js module, as @typedefs, and import them with @import in each component's script, as Cart.svelte does with CartLine. That module is checked like any other JavaScript.

Sources

Last reviewed 21 October 2026.