sveld Svelte Themes

Sveld

Generate TypeScript definitions and component documentation for your Svelte components

sveld

sveld generates TypeScript definitions and component documentation (Markdown/JSON) for Svelte components. It analyzes props, events, slots, and other component features through static analysis. Types and signatures can be defined using JSDoc notation.

The purpose of this project is to make third party Svelte component libraries compatible with the Svelte Language Server and TypeScript with minimal effort required by the author. For example, TypeScript definitions may be used during development via intelligent code completion in Integrated Development Environments (IDEs) like VSCode.

Carbon Components Svelte uses this library to auto-generate component types and API metadata.

sveld uses the Svelte 5 compiler to parse .svelte files. That single parse path powers docgen and TypeScript output for Svelte 3, Svelte 4, Svelte 5 without runes (export let, <slot>, $$restProps, …), and Svelte 5 Runes ($props(), $bindable(), {@render ...}, callback props such as onclick, …).

For lang="ts" components, sveld preserves source-level prop type annotations when possible instead of requiring JSDoc as the primary source of truth. This includes legacy export let props, typed $props() destructuring, typed whole-object $props() captures, local interface/type declarations, and imported type references in emitted .d.ts files.

Syntax mode Supported
Svelte 3
Svelte 4
Svelte 5 (non-Runes)
Svelte 5 Runes

Note that generated .d.ts files extend SvelteComponentTyped from svelte, so TypeScript and the Svelte Language Server work whether consumers use Svelte 3, Svelte 4, or Svelte 5.


Given a Svelte component, sveld can infer basic prop types to generate TypeScript definitions compatible with the Svelte Language Server:

Button.svelte

<script>
  export let type = "button";
  export let primary = false;
</script>

<button {...$$restProps} {type} class:primary on:click>
  <slot>Click me</slot>
</button>

The following generated .d.ts extends SvelteComponentTyped:

Button.svelte.d.ts

import { SvelteComponentTyped } from "svelte";
import type { SvelteHTMLElements } from "svelte/elements";

type $RestProps = SvelteHTMLElements["button"];

type $Props = {
  /**
   * @default "button"
   */
  type?: string;

  /**
   * @default false
   */
  primary?: boolean;

  [key: `data-${string}`]: unknown;
};

export type ButtonProps = Omit<$RestProps, keyof $Props> & $Props;

export default class Button extends SvelteComponentTyped<
  ButtonProps,
  { click: WindowEventMap["click"] },
  { default: Record<string, never> }
> {}

Sometimes, inferring prop types is insufficient.

Prop/event/slot types and signatures can be augmented using JSDoc notations.

/** @type {"button" | "submit" | "reset"} */
export let type = "button";

/**
 * Set to `true` to use the primary variant
 */
export let primary = false;

The accompanying JSDoc annotations would generate the following:

import type { SvelteHTMLElements } from "svelte/elements";

type $RestProps = SvelteHTMLElements["button"];

type $Props = {
  /**
   * @default "button"
   */
  type?: "button" | "submit" | "reset";

  /**
   * Set to `true` to use the primary variant
   * @default false
   */
  primary?: boolean;
};

export type ButtonProps = Omit<$RestProps, keyof $Props> & $Props;

export default class Button extends SvelteComponentTyped<
  ButtonProps,
  { click: WindowEventMap["click"] },
  { default: Record<string, never> }
> {}

Table of Contents

Approach

sveld uses the Svelte compiler to statically analyze Svelte components exported from a library to generate documentation useful to the end user.

Extracted metadata include:

  • props
  • slots
  • forwarded events
  • dispatched events
  • context (setContext/getContext)
  • $$restProps

This library adopts a progressively enhanced approach. Any property type that cannot be inferred (e.g., "hello" is a string) falls back to "any" to minimize incorrectly typed properties or signatures. To mitigate this, the library author can add JSDoc annotations to specify types that cannot be reliably inferred. This represents a progressively enhanced approach because JSDocs are comments that can be ignored by the compiler.

When both TypeScript syntax and JSDoc are present, sveld resolves prop types in this order:

  1. explicit TypeScript annotation
  2. explicit JSDoc annotation
  3. initializer inference
  4. any

sveld intentionally stays AST-only. It preserves imported and local type text in generated .d.ts output, but it does not perform project-wide semantic resolution with the TypeScript compiler. That means opaque imported whole-object $props() types can be preserved in declarations without being fully expanded into JSON metadata.

Usage

Installation

Install sveld as a development dependency.

# npm
npm i -D sveld

# pnpm
pnpm i -D sveld

# Bun
bun i -D sveld

# Yarn
yarn add -D sveld

Vite

Import and add sveld as a plugin to your vite.config.ts. The plugin only runs during vite build (not the dev server).

// vite.config.ts
import { svelte } from "@sveltejs/vite-plugin-svelte";
import sveld from "sveld";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [svelte(), sveld()],
});

Since Vite uses Rollup for production builds, sveld also works in Rollup configurations.

By default, sveld will use the "svelte" field from your package.json to determine the entry point. You can override this by specifying an explicit entry option:

sveld({
  entry: "src/index.js",
});

When building the library, TypeScript definitions are emitted to the types folder by default.

Customize the output folder using the typesOptions.outDir option. Use typesOptions.printWidth to control Prettier wrapping for generated .d.ts files. The default is 80.

The following example emits the output to the dist folder:

sveld({
+  typesOptions: {
+    outDir: 'dist',
+    printWidth: 80
+  }
})

CLI

The CLI uses the "svelte" field from your package.json as the entry point:

npx sveld

Generate documentation in JSON and/or Markdown formats using the following flags:

npx sveld --json --markdown

Node.js

You can also use sveld programmatically in Node.js. The package is ESM-only; require("sveld") is not supported. Use import or dynamic import().

If no input is specified, sveld will infer the entry point based on the package.json#svelte field.

import { sveld } from "sveld";
import pkg from "./package.json" with { type: "json" };

sveld({
  input: "./src/index.js",
  glob: true,
  markdown: true,
  markdownOptions: {
    onAppend: (type, document, components) => {
      if (type === "h1")
        document.append(
          "quote",
          `${components.size} components exported from ${pkg.name}@${pkg.version}.`,
        );
    },
  },
  json: true,
  jsonOptions: {
    outFile: "docs/src/COMPONENT_API.json",
  },
});

jsonOptions.outDir

If json is true, a COMPONENT_API.json file will be generated at the root of your project. This file contains documentation for all components.

Use the jsonOptions.outDir option to specify the folder for individual JSON files to be emitted.

sveld({
  json: true,
  jsonOptions: {
    // an individual JSON file will be generated for each component API
    // e.g. "docs/Button.api.json"
    outDir: "docs",
  },
});

Publishing to NPM

TypeScript definitions are outputted to the types folder by default. Don't forget to include the folder in your package.json when publishing the package to NPM.

{
  "svelte": "./src/index.js",
  "main": "./lib/index.mjs",
+ "types": "./types/index.d.ts",
  "files": [
    "src",
    "lib",
+   "types",
  ]
}

Available Options

Plugin Options

  • entry (string, optional): Specify the entry point to uncompiled Svelte source. If not provided, sveld will use the "svelte" field from package.json.
  • glob (boolean, optional): Enable glob mode to analyze all *.svelte files.
  • types (boolean, optional, default: true): Generate TypeScript definitions.
  • typesOptions (object, optional): Options for TypeScript definition generation, including outDir, preamble, and printWidth.
  • json (boolean, optional): Generate component documentation in JSON format.
  • jsonOptions (object, optional): Options for JSON output.
  • markdown (boolean, optional): Generate component documentation in Markdown format.
  • markdownOptions (object, optional): Options for Markdown output.

By default, only TypeScript definitions are generated.

To generate documentation in Markdown and JSON formats, set markdown and json to true.

sveld({
+  markdown: true,
+  json: true,
})

API Reference

reactive

The reactive field in generated JSON is heuristic metadata. It is not a complete statement of whether a parent may use bind:prop in Svelte.

sveld marks reactive: true when it finds internal evidence that a prop is writable, including:

  • the prop is assigned or mutated inside the component
  • the prop is marked bindable in runes mode with $bindable(...)
  • the prop is used as the target of bind:* on an element or child component
  • wrapper-forwarded bindings such as bind:value, bind:selected, and bind:ref

Local variables or parameters that shadow a prop name do not count as writes to the exported prop.

reactive: false means sveld found no such evidence. It does not imply that parent-side bind: usage is impossible.

For stable output, generated events arrays are emitted in deterministic sorted order.

@type

Without a @type annotation, sveld will infer the primitive type for a prop:

export let kind = "primary";
// inferred type: "string"

For template literal default values, sveld infers the type as string:

export let id = `ccs-${Math.random().toString(36)}`;
// inferred type: "string"

Use the @type tag to explicitly document the type. In the following example, the kind property has an enumerated (enum) type.

For lang="ts" components, prefer native TypeScript annotations when they are already present. @type remains useful for JavaScript components, for overriding inferred types, and for cases where the AST cannot recover a more precise type.

Signature:

/**
 * Optional description
 * @type {Type}
 */

Example:

Svelte 5 Runes:

<script>
  let {
    /**
     * Specify the kind of button
     * @type {"primary" | "secondary" | "tertiary"}
     */
    kind = "primary",
    /**
     * Specify the Carbon icon to render
     * @type {typeof import("carbon-icons-svelte").CarbonIcon}
     */
    renderIcon = Close20,
  } = $props();
</script>

For runes components with multiple destructured props, place JSDoc on the individual property you want to document. A declaration-level JSDoc block is only used as a fallback when the destructure exposes a single public prop.

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * Specify the kind of button
   * @type {"primary" | "secondary" | "tertiary"}
   */
  export let kind = "primary";

  /**
   * Specify the Carbon icon to render
   * @type {typeof import("carbon-icons-svelte").CarbonIcon}
   */
  export let renderIcon = Close20;
</script>

@default

By default, sveld infers the @default value from the prop's initializer and includes it in the generated TypeScript definitions:

<script>
  export let open = false;
</script>
/**
 * @default false
 */
open?: boolean;

Use the @default tag to explicitly document the default value. When an explicit @default annotation is provided, sveld uses it instead of the inferred value, avoiding duplicate @default tags in the output.

This is useful when the initializer references a variable or expression that is not meaningful to consumers:

<script>
  const defaultFilter = () => true;

  /**
   * @default () => true
   * @type {(item: string, value: string) => boolean}
   */
  export let shouldFilter = defaultFilter;
</script>
/**
 * @default () => true
 */
shouldFilter?: (item: string, value: string) => boolean;

Identifier resolution

When a prop's initializer is a variable reference, sveld resolves it to the actual value automatically:

<script>
  const DEFAULT_SIZE = "md";

  /** @type {"sm" | "md" | "lg"} */
  export let size = DEFAULT_SIZE;
</script>
/**
 * @default "md"
 */
size?: "sm" | "md" | "lg";

Chained references are also resolved:

<script>
  const ACTUAL_VALUE = 42;
  const ALIAS = ACTUAL_VALUE;

  export let count = ALIAS;
</script>
/**
 * @default 42
 */
count?: number;

Resolution follows up to 5 levels of indirection. Beyond that, the last resolved identifier name is used as the default value. If the identifier cannot be resolved (e.g., it is imported from another module), the variable name is used as-is.

When an explicit @default annotation is provided, it always takes precedence over the resolved value.

@typedef

The @typedef tag can be used to define a common type that is used multiple times within a component. All typedefs defined in a component will be exported from the generated TypeScript definition file.

Signature:

/**
 * @typedef {Type} TypeName
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * @typedef {string} AuthorName
   * @typedef {{ name?: AuthorName; dob?: string; }} Author
   */

  let {
    /** @type {Author} */
    author = {},
    /** @type {Author[]} */
    authors = [],
  } = $props();
</script>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * @typedef {string} AuthorName
   * @typedef {{ name?: AuthorName; dob?: string; }} Author
   */

  /** @type {Author} */
  export let author = {};

  /** @type {Author[]} */
  export let authors = [];
</script>

Using @property for complex typedefs

For complex object types, use the @property tag to document individual properties. This provides better documentation and IDE support with per-property tooltips.

Signature:

/**
 * Type description
 * @typedef {object} TypeName
 * @property {Type} propertyName - Property description
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * Represents a user in the system
   * @typedef {object} User
   * @property {string} name - The user's full name
   * @property {string} email - The user's email address
   * @property {number} age - The user's age in years
   */

  /** @type {User} */
  let { user = { name: "John", email: "[email protected]", age: 30 } } = $props();
</script>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * Represents a user in the system
   * @typedef {object} User
   * @property {string} name - The user's full name
   * @property {string} email - The user's email address
   * @property {number} age - The user's age in years
   */

  /** @type {User} */
  export let user = { name: "John", email: "[email protected]", age: 30 };
</script>

Output:

export type User = {
  /** The user's full name */
  name: string;
  /** The user's email address */
  email: string;
  /** The user's age in years */
  age: number;
};

export type ComponentProps = {
  /**
   * Represents a user in the system
   * @default { name: "John", email: "[email protected]", age: 30 }
   */
  user?: User;
};

Optional properties and default values

Following JSDoc standards, use square brackets to mark properties as optional. You can also specify default values using the [propertyName=defaultValue] syntax.

Signature:

/**
 * @typedef {object} TypeName
 * @property {Type} [optionalProperty] - Optional property description
 * @property {Type} [propertyWithDefault=defaultValue] - Property with default value
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * Configuration options for the component
   * @typedef {object} ComponentConfig
   * @property {boolean} enabled - Whether the component is enabled
   * @property {string} theme - The component theme
   * @property {number} [timeout=5000] - Optional timeout in milliseconds
   * @property {boolean} [debug] - Optional debug mode flag
   */

  /** @type {ComponentConfig} */
  let { config = { enabled: true, theme: "dark" } } = $props();
</script>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * Configuration options for the component
   * @typedef {object} ComponentConfig
   * @property {boolean} enabled - Whether the component is enabled
   * @property {string} theme - The component theme
   * @property {number} [timeout=5000] - Optional timeout in milliseconds
   * @property {boolean} [debug] - Optional debug mode flag
   */

  /** @type {ComponentConfig} */
  export let config = { enabled: true, theme: "dark" };
</script>

Output:

export type ComponentConfig = {
  /** Whether the component is enabled */
  enabled: boolean;
  /** The component theme */
  theme: string;
  /** Optional timeout in milliseconds @default 5000 */
  timeout?: number;
  /** Optional debug mode flag */
  debug?: boolean;
};

export type ComponentProps = {
  /**
   * Configuration options for the component
   * @default { enabled: true, theme: "dark" }
   */
  config?: ComponentConfig;
};

Note: The inline syntax @typedef {{ name: string }} User continues to work for backwards compatibility.

@callback

The @callback tag defines a function type using @param and @returns tags, following the TypeScript JSDoc @callback specification. Like @typedef, callbacks are exported from the generated TypeScript definition file.

This is useful for typing callback props without using inline function type syntax.

Signature:

/**
 * Optional description
 * @callback CallbackName
 * @param {Type} paramName - Parameter description
 * @returns {ReturnType}
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * Callback fired when the value changes
   * @callback OnChange
   * @param {string} value - The new value
   * @param {number} index - The index of the changed item
   * @returns {void}
   */

  /** @type {OnChange} */
  let { onChange = (value, index) => {} } = $props();
</script>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * Callback fired when the value changes
   * @callback OnChange
   * @param {string} value - The new value
   * @param {number} index - The index of the changed item
   * @returns {void}
   */

  /** @type {OnChange} */
  export let onChange = (value, index) => {};
</script>

Output:

/**
 * Callback fired when the value changes
 */
export type OnChange = (value: string, index: number) => void;

export type ComponentProps = {
  /**
   * Callback fired when the value changes
   */
  onChange?: OnChange;
};

Callbacks can be combined with @typedef in the same comment block:

/**
 * @typedef {"asc" | "desc"} SortDirection
 * @callback SortFn
 * @param {any} a
 * @param {any} b
 * @param {SortDirection} direction
 * @returns {number}
 */

When @returns is omitted, the return type defaults to void. When no @param tags are present, the callback is typed as a no-argument function.

@slot / @snippet

Use the @slot tag for typing component slots. For Svelte 5 runes components, @snippet is also supported as an alias. Both are non-standard JSDoc tags.

Descriptions are optional for named slots. Currently, the default slot cannot have a description.

Signature:

/**
 * @slot {Type} slot-name [slot description]
 * @snippet {Type} snippet-name [snippet description]
 */

Omit the `slot-name` to type the default slot.

/**
 * @slot {Type}
 * @snippet {Type}
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * @snippet {{ prop: number; doubled: number; }}
   * @snippet {{}} title
   * @snippet {{ prop: number }} body - Customize the paragraph text.
   */

  let { prop = 0, children, title, body } = $props();
</script>

<h1>
  {@render children?.({ prop, doubled: prop * 2 })}
  {@render title?.()}
</h1>

<p>
  {@render body?.({ prop })}
</p>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * @slot {{ prop: number; doubled: number; }}
   * @slot {{}} title
   * @slot {{ prop: number }} body - Customize the paragraph text.
   */

  export let prop = 0;
</script>

<h1>
  <slot {prop} doubled={prop * 2} />
  <slot name="title" />
</h1>

<p>
  <slot name="body" {prop} />
</p>

Svelte 5 Snippet Compatibility

For Svelte 5 compatibility, sveld automatically generates optional snippet props for all slots. This allows consumers to use either the traditional slot syntax or Svelte 5's {#snippet} syntax.

When parsing runes components, sveld maps {@render ...} calls back into the same slot metadata used for traditional <slot> declarations. Reserved snippet props such as children, along with named snippet props discovered from {@render ...}, are represented through slots metadata and generated snippet prop types rather than duplicated in the props output.

Positional snippet calls such as {@render row?.(item, index)} are preserved as typed props when the prop itself has an explicit type like Snippet<[Item, number]>. They are not converted into synthetic slot metadata.

For slots with props (e.g., let:prop), the generated type uses a Snippet-compatible signature:

slotName?: (this: void, ...args: [{ prop: PropType }]) => void;

For slots without props:

slotName?: (this: void) => void;

Why this signature?

  • this: void – Ensures the snippet cannot be called with a this context, matching Svelte's internal enforcement that snippets are pure render functions
  • ...args: [Props] – Uses tuple spread for type-safe parameters. This accepts fixed-length tuples (like [{ row: Row }]) while rejecting array types (like Props[]), matching how Svelte's Snippet<T> type works

Default slot (children prop):

The default slot generates an optional children snippet prop:

<!-- Component with default slot that passes props -->
<Dropdown {items} selectedId="1">
  {#snippet children({ item, index })}
    <span>{item.text} (#{index})</span>
  {/snippet}
</Dropdown>

Generated types:

type DropdownProps = {
  items: Item[];
  selectedId?: string;

  // Default slot as children snippet prop
  children?: (this: void, ...args: [{ item: Item; index: number }]) => void;
};

Named slots:

<!-- Using the generated types with Svelte 5 syntax -->
<DataTable headers={headers} rows={rows}>
  {#snippet cell({ cell, row })}
    {#if cell.key === 'actions'}
      <Button on:click={() => handleAction(row)}>Edit</Button>
    {:else}
      {cell.value}
    {/if}
  {/snippet}
</DataTable>

The generated TypeScript definition includes both the snippet prop and the traditional slot definition:

type DataTableProps<Row> = {
  // ... other props

  // Snippet prop for Svelte 5 compatibility
  cell?: (
    this: void,
    ...args: [
      {
        row: Row;
        cell: DataTableCell<Row>;
        rowIndex: number;
        cellIndex: number;
      },
    ]
  ) => void;

  // Default slot as children prop
  children?: (this: void) => void;
};

export default class DataTable<Row> extends SvelteComponentTyped<
  DataTableProps<Row>,
  {
    /* events */
  },
  {
    // Traditional slot definition (Svelte 3/4)
    default: Record<string, never>;
    cell: {
      row: Row;
      cell: DataTableCell<Row>;
      rowIndex: number;
      cellIndex: number;
    };
  }
> {}

@event

Use the @event tag to type dispatched events. An event name is required and a description optional.

In Svelte 5 runes components, callback props such as onclick are treated as component props, not events. The events output remains reserved for real dispatched events and legacy forwarded events. If a runes component documents @event foo and exposes a matching callback prop like onfoo without actually dispatching or forwarding foo, sveld aliases that documentation onto the callback prop instead of synthesizing an emitted event.

Use null as the value if no event detail is provided.

Signature:

/**
 * Optional event description
 * @event {EventDetail} eventname [inline description]
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * Fired when a value is saved.
   * @event {{ id: string }} save
   */
  let { onsave } = $props();
</script>

<button onclick={() => onsave?.({ id: "1" })}>Save</button>

Svelte 5 Runes with dispatched events:

<script>
  /**
   * @event {{ key: string }} button:key
   * @event {null} key - Fired when `key` changes.
   */

  let { key = "" } = $props();

  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  $effect(() => {
    dispatch("button:key", { key });
    if (key) dispatch("key");
  });
</script>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * @event {{ key: string }} button:key
   * @event {null} key - Fired when `key` changes.
   */

  export let key = "";

  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  $: dispatch("button:key", { key });
  $: if (key) dispatch("key");
</script>

Output:

export default class Component extends SvelteComponentTyped<
  ComponentProps,
  {
    "button:key": CustomEvent<{ key: string }>;
    /** Fired when `key` changes. */ key: CustomEvent<null>;
  },
  Record<string, never>
> {}

Using @property for complex event details

For events with complex object payloads, use the @property tag to document individual properties. The main comment description will be used as the event description.

Signature:

/**
 * Event description
 * @event eventname
 * @type {object}
 * @property {Type} propertyName - Property description
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * Fired when the user submits the form
   *
   * @event submit
   * @type {object}
   * @property {string} name - The user's name
   * @property {string} email - The user's email address
   * @property {boolean} newsletter - Whether the user opted into the newsletter
   */

  let { name = "Jane Doe", email = "[email protected]", newsletter = true } = $props();

  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function handleSubmit() {
    dispatch("submit", { name, email, newsletter });
  }
</script>

<button type="button" onclick={handleSubmit}>Submit</button>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * Fired when the user submits the form
   *
   * @event submit
   * @type {object}
   * @property {string} name - The user's name
   * @property {string} email - The user's email address
   * @property {boolean} newsletter - Whether the user opted into the newsletter
   */

  export let name = "Jane Doe";
  export let email = "[email protected]";
  export let newsletter = true;

  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function handleSubmit() {
    dispatch("submit", { name, email, newsletter });
  }
</script>

<button type="button" on:click={handleSubmit}>Submit</button>

Output:

export default class Component extends SvelteComponentTyped<
  ComponentProps,
  {
    /** Fired when the user submits the form */
    submit: CustomEvent<{
      /** The user's name */
      name: string;
      /** The user's email address */
      email: string;
      /** Whether the user opted into the newsletter */
      newsletter: boolean;
    }>;
  },
  Record<string, never>
> {}

Optional properties in event details

Just like with typedefs, you can mark event detail properties as optional using square brackets. This is useful when some properties may not always be included in the event payload.

Example:

Svelte 5 Runes:

<script>
  /**
   * Snowball event fired when throwing a snowball
   *
   * @event snowball
   * @type {object}
   * @property {boolean} isPacked - Indicates whether the snowball is tightly packed
   * @property {number} speed - The speed of the snowball in mph
   * @property {string} [color] - Optional color of the snowball
   * @property {number} [density=0.9] - Optional density with default value
   */

  let { speed = 50 } = $props();

  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function throwSnowball() {
    dispatch("snowball", {
      isPacked: true,
      speed,
    });
  }
</script>

<button type="button" onclick={throwSnowball}>Throw</button>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * Snowball event fired when throwing a snowball
   *
   * @event snowball
   * @type {object}
   * @property {boolean} isPacked - Indicates whether the snowball is tightly packed
   * @property {number} speed - The speed of the snowball in mph
   * @property {string} [color] - Optional color of the snowball
   * @property {number} [density=0.9] - Optional density with default value
   */

  export let speed = 50;

  import { createEventDispatcher } from "svelte";

  const dispatch = createEventDispatcher();

  function throwSnowball() {
    dispatch("snowball", {
      isPacked: true,
      speed,
    });
  }
</script>

<button type="button" on:click={throwSnowball}>Throw</button>

Output:

export default class Component extends SvelteComponentTyped<
  ComponentProps,
  {
    /** Snowball event fired when throwing a snowball */
    snowball: CustomEvent<{
      /** Indicates whether the snowball is tightly packed */
      isPacked: boolean;
      /** The speed of the snowball in mph */
      speed: number;
      /** Optional color of the snowball */
      color?: string;
      /** Optional density with default value @default 0.9 */
      density?: number;
    }>;
  },
  Record<string, never>
> {}

Context API

sveld automatically generates TypeScript definitions for Svelte's setContext/getContext API by extracting types from JSDoc annotations on the context values.

How it works

When you use setContext in a component, sveld will:

  1. Detect the setContext call
  2. Extract the context key (must be a string literal)
  3. Find JSDoc @type annotations on the variables being passed
  4. Generate a TypeScript type export for the context

Example

Modal.svelte

Svelte 5 Runes:

<script>
  import { setContext } from "svelte";

  /**
   * Close the modal
   * @type {() => void}
   */
  const close = () => {
    // Close logic
  };

  /**
   * Open the modal with content
   * @type {(component: any, props?: any) => void}
   */
  const open = (component, props) => {
    // Open logic
  };

  setContext("simple-modal", { open, close });

  let { children } = $props();
</script>

<div class="modal">
  {@render children?.()}
</div>

Svelte 3, 4, 5 (non-Runes):

<script>
  import { setContext } from "svelte";

  /**
   * Close the modal
   * @type {() => void}
   */
  const close = () => {
    // Close logic
  };

  /**
   * Open the modal with content
   * @type {(component: any, props?: any) => void}
   */
  const open = (component, props) => {
    // Open logic
  };

  setContext("simple-modal", { open, close });
</script>

<div class="modal">
  <slot />
</div>

Generated TypeScript definition:

export type SimpleModalContext = {
  /** Open the modal with content */
  open: (component: any, props?: any) => void;
  /** Close the modal */
  close: () => void;
};

export type ModalProps = {};

export default class Modal extends SvelteComponentTyped<
  ModalProps,
  Record<string, any>,
  { default: Record<string, never> }
> {}

Consumer usage:

<script>
  import { getContext } from 'svelte';
  import type { SimpleModalContext } from 'modal-library/Modal.svelte';

  // Fully typed with autocomplete!
  const { close, open } = getContext<SimpleModalContext>('simple-modal');
</script>

<button on:click={close}>Close</button>

Explicitly typing contexts

There are several ways to provide type information for contexts:

Option 1: Inline JSDoc on variables (recommended)

<script>
  import { setContext } from 'svelte';

  /**
   * @type {() => void}
   */
  const close = () => {};

  setContext('modal', { close });
</script>

Option 2: Using @typedef for complex types

<script>
  import { setContext } from 'svelte';

  /**
   * @typedef {object} TabData
   * @property {string} id
   * @property {string} label
   * @property {boolean} [disabled]
   */

  /**
   * @type {(tab: TabData) => void}
   */
  const addTab = (tab) => {};

  setContext('tabs', { addTab });
</script>

Option 3: Referencing imported types

<script>
  import { setContext } from 'svelte';

  /**
   * @type {typeof import("./types").ModalAPI}
   */
  const modalAPI = {
    open: () => {},
    close: () => {}
  };

  setContext('modal', modalAPI);
</script>

Option 4: Direct object literal with inline functions

<script>
  import { setContext } from 'svelte';

  // sveld infers basic function signatures
  setContext('modal', {
    open: (component, props) => {}, // Inferred as (arg, arg) => any
    close: () => {}                 // Inferred as () => any
  });
</script>

Note: For best results, use explicit JSDoc @type annotations. Inline functions without annotations will be inferred with generic signatures.

Notes

  • Context keys must be string literals (dynamic keys are not supported)
  • Variables passed to setContext should have JSDoc @type annotations for accurate types
  • The generated type name follows the pattern: {PascalCase}Context. Separators (hyphens, underscores, dots, colons, slashes, spaces) are stripped and each segment is capitalized:
    Context Key Generated Type Name
    "simple-modal" SimpleModalContext
    "user_settings" UserSettingsContext
    "Carbon.Modal" CarbonModalContext
    "Carbon:Modal" CarbonModalContext
    "app/modal" AppModalContext
    "My Context" MyContextContext
    "Tabs" TabsContext
  • If no type annotation is found, the type defaults to any with a warning

@restProps

sveld can pick up inline HTML elements that $$restProps is forwarded to. However, it cannot infer the underlying element for instantiated components.

You can use the @restProps tag to specify the element tags that $$restProps is forwarded to.

Signature:

/**
 * Single element
 * @restProps {tagname}
 *
 * Multiple elements
 * @restProps {tagname-1 | tagname-2 | tagname-3}
 */

Example:

Svelte 5 Runes:

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

  /** @restProps {h1 | button} */
  let { edit = false, children, ...restProps } = $props();
</script>

{#if edit}
  <Button {...restProps} />
{:else}
  <h1 {...restProps}>
    {@render children?.()}
  </h1>
{/if}

Svelte 3, 4, 5 (non-Runes):

<script>
  /** @restProps {h1 | button} */
  export let edit = false;

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

{#if edit}
  <Button {...$$restProps} />
{:else}
  <h1 {...$$restProps}><slot /></h1>
{/if}

@extendProps

In some cases, a component may be based on another component. The @extendProps tag can be used to extend generated component props.

Note: @extends is supported as an alias but @extendProps is preferred to avoid conflicts with standard JSDoc @extends (used for class inheritance).

Signature:

/**
 * @extendProps {<relative path to component>} ComponentProps
 */

Example:

/** @extendProps {"./Button.svelte"} ButtonProps */

export const secondary = true;

import Button from "./Button.svelte";

@generics

Currently, to define generics for a Svelte component, you must use generics attribute on the script tag. Note that this feature is experimental and may change in the future.

However, the generics attribute only works if using lang="ts"; the language server will produce an error if generics is used without specifying lang="ts".

<!-- This causes an error because `lang="ts"` must be used. -->
<script generics="Row extends DataTableRow = any"></script>

Because sveld is designed to support JavaScript-only usage as a baseline, the API design to specify generics uses a custom JSDoc tag @generics.

Signature:

/**
 * @generics {GenericParameter} GenericName
 */

Example:

/**
 * @generics {Row extends DataTableRow = any} Row
 */

Component example:

Svelte 5 Runes:

<script>
  /**
   * @typedef {{ id: string | number; [key: string]: any; }} DataTableRow
   * @typedef {Exclude<keyof Row, "id">} DataTableKey<Row>
   * @typedef {{ key: DataTableKey<Row>; value: string; }} DataTableHeader<Row=DataTableRow>
   * @template {DataTableRow} <Row extends DataTableRow = DataTableRow>
   * @generics {Row extends DataTableRow = DataTableRow} Row
   */

  let {
    /** @type {ReadonlyArray<DataTableHeader<Row>>} */
    headers = [],
    /** @type {ReadonlyArray<Row>} */
    rows = [],
    children,
  } = $props();
</script>

{@render children?.({ headers, rows })}

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * @typedef {{ id: string | number; [key: string]: any; }} DataTableRow
   * @typedef {Exclude<keyof Row, "id">} DataTableKey<Row>
   * @typedef {{ key: DataTableKey<Row>; value: string; }} DataTableHeader<Row=DataTableRow>
   * @template {DataTableRow} <Row extends DataTableRow = DataTableRow>
   * @generics {Row extends DataTableRow = DataTableRow} Row
   */

  /** @type {ReadonlyArray<DataTableHeader<Row>>} */
  export let headers = [];

  /** @type {ReadonlyArray<Row>} */
  export let rows = [];
</script>

<slot {headers} {rows} />

The generated TypeScript definition will resemble the following:

// Props type includes the full constraint, enabling indexed access types like Row["id"]
export type ComponentProps<Row extends DataTableRow = any> = {
  rows?: ReadonlyArray<Row>;
};

export default class Component<
  Row extends DataTableRow = any,
> extends SvelteComponentTyped<
  ComponentProps<Row>,
  Record<string, any>,
  Record<string, any>
> {}

For a parameter list, the name should be comma-separated but not include spaces.

/**
 * @generics {Param1, Param2} Name1,Name2
 */
export type ComponentProps<Param1, Param2> = { ... };

export default class Component<Param1, Param2> extends SvelteComponentTyped<
  ComponentProps<Name1, Name2>,
  Record<string, any>,
  Record<string, any>
> {}

@component comments

The Svelte Language Server supports component-level comments through the following syntax: <!-- @component [comment] -->.

sveld will copy these over to the exported default component in the TypeScript definition.

Example:

Svelte 5 Runes:

<!-- @component
@example
<Button>
  Text
</Button>
-->
<script>
  let { children } = $props();
</script>

<button>
  {@render children?.()}
</button>

Svelte 3, 4, 5 (non-Runes):

<!-- @component
@example
<Button>
  Text
</Button>
-->
<button>
  <slot />
</button>

Output:

/**
 * @example
 * <Button>
 *   Text
 * </Button>
 */
export default class Button extends SvelteComponentTyped<
  ButtonProps,
  Record<string, any>,
  { default: Record<string, never> }
> {}

Accessor Props

Exported functions and consts become accessor props in generated TypeScript definitions. Use @type to document function signatures, or use @param and @returns (or @return) JSDoc tags for richer documentation.

Note that @type tag annotations take precedence over @param/@returns tags.

Signature:

/**
 * Function description
 * @param {Type} paramName - Parameter description
 * @param {Type} [optionalParam] - Optional parameter
 * @returns {ReturnType} Return value description
 */

Example:

Svelte 5 Runes:

<script>
  /**
   * @typedef {object} NotificationData
   * @property {string} [id] - Optional id for deduplication
   * @property {"error" | "info" | "success"} [kind]
   */

  let { children } = $props();

  /**
   * Add a notification to the queue.
   * @param {NotificationData} notification
   * @returns {string} The notification id
   */
  export function add(notification) {
    const id = notification.id ?? "id";
    return id;
  }

  /**
   * Remove a notification by id.
   * @param {string} id
   * @returns {boolean} True if the notification was found and removed
   */
  export function remove(id) {
    return true;
  }

  /**
   * Get notification count.
   * @returns {number} The number of notifications
   */
  export function getCount() {
    return 0;
  }
</script>

<div>
  {@render children?.()}
</div>

Svelte 3, 4, 5 (non-Runes):

<script>
  /**
   * @typedef {object} NotificationData
   * @property {string} [id] - Optional id for deduplication
   * @property {"error" | "info" | "success"} [kind]
   */

  /**
   * Add a notification to the queue.
   * @param {NotificationData} notification
   * @returns {string} The notification id
   */
  export function add(notification) {
    const id = notification.id ?? "id";
    return id;
  }

  /**
   * Remove a notification by id.
   * @param {string} id
   * @returns {boolean} True if the notification was found and removed
   */
  export function remove(id) {
    return true;
  }

  /**
   * Get notification count.
   * @returns {number} The number of notifications
   */
  export function getCount() {
    return 0;
  }
</script>

Output:

export type NotificationData = {
  /** Optional id for deduplication */
  id?: string;
  kind?: "error" | "info" | "success";
};

export type ComponentProps = Record<string, never>;

export default class Component extends SvelteComponentTyped<
  ComponentProps,
  Record<string, any>,
  Record<string, never>
> {
  /**
   * Add a notification to the queue.
   */
  add: (notification: NotificationData) => string;

  /**
   * Remove a notification by id.
   */
  remove: (id: string) => boolean;

  /**
   * Get notification count.
   */
  getCount: () => number;
}

When only @param tags are present without @returns, the return type defaults to any. When only @returns is present without @param, the function signature is () => returnType.

Contributing

Refer to the contributing guidelines.

License

Apache-2.0

Top categories

Loading Svelte Themes