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:
/**
* @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);
}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):
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'.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
.sveltefiles ininclude. svelte-check reads theincludelist ofjsconfig.json; in the one-off run, leavingsrc/**/*.svelteout 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 wereexport letdeclarations, typed with a@typeabove each one; the$props()form is Svelte 5. - Events are props in Svelte 5. A callback prop such as
oncheckoutis 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
- TypeScript (Svelte docs) · Svelte docs
- $props · Svelte docs
- sv check · Svelte docs
Last reviewed 21 October 2026.