sveld is a TypeScript definition generator 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 tool can also generate component documentation in Markdown and JSON formats.
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 (IDE) like VSCode.
Carbon Components Svelte uses this library to auto-generate component types and API metadata:
- TypeScript definitions: Component TypeScript definitions
- Component Index: Markdown file documenting component props, slots, and events
- Component API: Component API metadata in JSON format
Please note that the generated TypeScript definitions require Svelte version 3.55 or greater.
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 generated definition extends the official SvelteComponentTyped interface exported from Svelte.
Button.svelte.d.ts
import type { 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}`]: any;
};
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> }
> {}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.
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 sveldImport and add sveld as a plugin to your rollup.config.js.
// rollup.config.js
import svelte from "rollup-plugin-svelte";
import resolve from "@rollup/plugin-node-resolve";
import sveld from "sveld";
export default {
input: "src/index.js",
output: {
format: "es",
file: "lib/index.mjs",
},
plugins: [svelte(), resolve(), sveld()],
};When building the library, TypeScript definitions are emitted to the types folder by default.
Customize the output folder using the typesOptions.outDir option.
The following example emits the output to the dist folder:
sveld({
+ typesOptions: {
+ outDir: 'dist'
+ }
})The tests/e2e folder contains example set-ups:
- single-export: library that exports one component
- single-export-default-only: library that exports one component using the concise
export { default } ...syntax - multi-export: multi-component library without JSDoc annotations (types are inferred)
- multi-export-typed: multi-component library with JSDoc annotations
- multi-export-typed-ts-only: multi-component library that only generates TS definitions
- glob: library that uses the glob strategy to collect/analyze *.svelte files
- carbon: full
carbon-components-svelteexample
The CLI uses the "svelte" field from your package.json as the entry point:
npx sveldGenerate documentation in JSON and/or Markdown formats using the following flags:
npx sveld --json --markdownYou can also use sveld programmatically in Node.js.
If no input is specified, sveld will infer the entry point based on the package.json#svelte field.
const { sveld } = require("sveld");
const pkg = require("./package.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",
},
});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",
},
});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",
]
}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,
})Without a @type annotation, sveld will infer the primitive type for a prop:
export let kind = "primary";
// inferred type: "string"Use the @type tag to explicitly document the type. In the following example, the kind property has an enumerated (enum) type.
Signature:
/**
* Optional description
* @type {Type}
*/Example:
/**
* 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;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:
/**
* @typedef {string} AuthorName
* @typedef {{ name?: AuthorName; dob?: string; }} Author
*/
/** @type {Author} */
export let author = {};
/** @type {Author[]} */
export let authors = [];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:
/**
* 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 };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: "john@example.com", age: 30 }
*/
user?: User;
};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:
/**
* 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" };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 }} Usercontinues to work for backwards compatibility.
Use the @slot tag for typing component slots. Note that @slot is a non-standard JSDoc tag.
Descriptions are optional for named slots. Currently, the default slot cannot have a description.
Signature:
/**
* @slot {Type} slot-name [slot description]
*/
Omit the `slot-name` to type the default slot.
/**
* @slot {Type}
*/Example:
<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>Use the @event tag to type dispatched events. An event name is required and a description optional.
Use null as the value if no event detail is provided.
Signature:
/**
* Optional event description
* @event {EventDetail} eventname [inline description]
*/Example:
/**
* @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");Output:
export default class Component extends SvelteComponentTyped<
ComponentProps,
{
"button:key": CustomEvent<{ key: string }>;
/** Fired when `key` changes. */ key: CustomEvent<null>;
},
Record<string, never>
> {}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:
/**
* 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
*/
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
function handleSubmit() {
dispatch("submit", {
name: "Jane Doe",
email: "[email protected]",
newsletter: true
});
}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>
> {}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:
/**
* 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
*/
import { createEventDispatcher } from "svelte";
const dispatch = createEventDispatcher();
function throwSnowball() {
dispatch("snowball", {
isPacked: true,
speed: 50
});
}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>
> {}sveld automatically generates TypeScript definitions for Svelte's setContext/getContext API by extracting types from JSDoc annotations on the context values.
When you use setContext in a component, sveld will:
- Detect the
setContextcall - Extract the context key (must be a string literal)
- Find JSDoc
@typeannotations on the variables being passed - Generate a TypeScript type export for the context
Modal.svelte
<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>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
@typeannotations. Inline functions without annotations will be inferred with generic signatures.
- Context keys must be string literals (dynamic keys are not supported)
- Variables passed to
setContextshould have JSDoc@typeannotations for accurate types - The generated type name follows the pattern:
{PascalCase}Context"simple-modal"→SimpleModalContext"Tabs"→TabsContext
- If no type annotation is found, the type defaults to
anywith a warning
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:
<script>
/** @restProps {h1 | button} */
export let edit = false;
import Button from "../";
</script>
{#if edit}
<Button {...$$restProps} />
{:else}
<h1 {...$$restProps}><slot /></h1>
{/if}In some cases, a component may be based on another component. The @extends tag can be used to extend generated component props.
Signature:
/**
* @extends {<relative path to component>} ComponentProps
*/Example:
/** @extends {"./Button.svelte"} ButtonProps */
export const secondary = true;
import Button from "./Button.svelte";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
*/The generated TypeScript definition will resemble the following:
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 default class Component<Param1, Param2> extends SvelteComponentTyped<
ComponentProps<Name1, Name2>,
Record<string, any>,
Record<string, any>
> {}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:
<!-- @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> }
> {}Refer to the contributing guidelines.