Skip to content

Commit 0e01dcf

Browse files
authored
Merge pull request #302 from storybookjs/jeppe/improve-types
Fix types
2 parents 8d4ea75 + d926543 commit 0e01dcf

27 files changed

+1661
-637
lines changed

README.md

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,6 @@ Similar to regular CSF, you can define a meta-level `render`-function, by refere
177177
import MyComponent from './MyComponent.svelte';
178178
179179
const { Story } = defineMeta({
180-
// @ts-expect-error -- TypeScript does not know this is valid: https://github.com/sveltejs/language-tools/issues/2653
181180
render: template,
182181
// 👆 the name of the snippet as defined below (can be any name)
183182
});
@@ -204,9 +203,6 @@ Stories can still override this default snippet using any of the methods for def
204203
> [!NOTE]
205204
> Svelte has the limitation, that you can't reference a snippet from a `<script module>` if it reference any declarations in a non-module `<script>` (whether directly or indirectly, via other snippets). See [svelte.dev/docs/svelte/snippet#Exporting-snippets](https://svelte.dev/docs/svelte/snippet#Exporting-snippets)
206205
207-
> [!IMPORTANT]
208-
> There is currently a bug in the Svelte language tools, which causes TypeScript to error with `TS(2448): Block-scoped variable 'SNIPPET_NAMAE' used before its declaration.`. Until that is fixed, you have to silent it with `//@ts-ignore` or `//@ts-expect-error`. See https://github.com/sveltejs/language-tools/issues/2653
209-
210206
#### Custom export name
211207

212208
Behind-the-scenes, each `<Story />` definition is compiled to a variable export like `export const MyStory = ...;`. In most cases you don't have to care about this detail, however sometimes naming conflicts can arise from this. The variable names are simplifications of the story names - to make them valid JavaScript variables.
@@ -237,35 +233,78 @@ If for some reason you need to access the [Story context](https://storybook.js.o
237233

238234
### TypeScript
239235

240-
Story snippets and args can be type-safe when necessary. The type of the args are inferred from the component props passed to `defineMeta`.
236+
Story template snippets can be type-safe when necessary. The type of the args are inferred from the `component` or `render` property passed to `defineMeta`.
241237

242-
You can make your snippets type-safe with the `Args` and `StoryContext` helper types:
238+
If you're just rendering the component directly without a custom template, you can use Svelte's `ComponentProps` type and `StoryContext` from the addon to make your template snippet type-safe:
243239

244240
```svelte
245241
<script module lang="ts">
246-
import { defineMeta, type Args, type StoryContext } from '@storybook/addon-svelte-csf';
247-
// 👆 👆 import those type helpers from this addon -->
242+
import { defineMeta, type StoryContext } from '@storybook/addon-svelte-csf';
243+
import { type ComponentProps } from 'svelte';
248244
249245
import MyComponent from './MyComponent.svelte';
250246
251247
const { Story } = defineMeta({
252248
component: MyComponent,
253249
});
250+
251+
type Args = ComponentProps<MyComponent>;
254252
</script>
255253
256-
<!-- 👇 use to infer `args` type from the `Story` component -->
257-
{#snippet template(args: Args<typeof Story>, context: StoryContext<typeof Story>)}
258-
<!-- 👆 use to infer `context` type from the `Story` component -->
254+
{#snippet template(args: Args, context: StoryContext<typeof Layout>)}
259255
<MyComponent {...args} />
260256
{/snippet}
261257
```
262258

263-
If you need to customize the type of the `args`, you can pass in a generic type parameter to `defineMeta` that will override the types inferred from the component:
259+
If you use the `render`-property to define a custom template that might use custom args, the args will be inferred from the types of the snippet passed to `render`. This is especially useful when you're converting primitive args to snippets:
264260

265261
```svelte
266-
const { Story } = defineMeta<{ anotherProp: boolean }>( ... );
262+
<script module lang="ts">
263+
import { defineMeta, type StoryContext } from '@storybook/addon-svelte-csf';
264+
import { type ComponentProps } from 'svelte';
265+
266+
import MyComponent from './MyComponent.svelte';
267+
268+
const { Story } = defineMeta({
269+
component: MyComponent,
270+
render: template, // 👈 args will be inferred from this, which is the Args type below
271+
argTypes: {
272+
children: {
273+
control: 'text',
274+
},
275+
footer: {
276+
control: 'text',
277+
},
278+
},
279+
});
280+
281+
type Args = Omit<ComponentProps<MyComponent>, 'children' | 'footer'> & {
282+
children: string;
283+
footer?: string;
284+
};
285+
// OR use the Merge helper from the 'type-fest' package:
286+
type Args = Merge<
287+
ComponentProps<MyComponent>,
288+
{
289+
children: string;
290+
footer?: string;
291+
}
292+
>;
293+
</script>
294+
295+
<!-- 👇 you need to omit 'children' from args to satisfy Svelte's constraints -->
296+
{#snippet template({ children, ...args }: Args, context: StoryContext<typeof MyComponent>)}
297+
<MyComponent {...args}>
298+
{children}
299+
{#snippet footer()}
300+
{args.footer}
301+
{/snippet}
302+
</MyComponent>
303+
{/snippet}
267304
```
268305

306+
See [the `Types.stories.svelte` examples](./examples/Types.stories.svelte) on how to use complex types properly.
307+
269308
### Legacy API
270309

271310
Version 5 of the addon changes the API from v4 in key areas, as described above. However a feature flag has been introduced to maintain support for the `<Template>`-based legacy API as it was prior to v5.

examples/Button.stories.svelte

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
<script module lang="ts">
2-
import { defineMeta, type Args, type StoryContext } from '@storybook/addon-svelte-csf';
2+
import { defineMeta, type StoryContext } from '@storybook/addon-svelte-csf';
33
import { fn } from '@storybook/test';
44
55
import Button from './components/Button.svelte';
6+
import type { ComponentProps } from 'svelte';
67
78
const onclickFn = fn().mockName('onclick');
89
@@ -25,12 +26,11 @@
2526
},
2627
children: { control: 'text' },
2728
},
28-
//@ts-expect-error TS does not understand that the snippet is defined before this call
2929
render: template,
3030
});
3131
</script>
3232

33-
{#snippet template(args: Args<typeof Story>, context: StoryContext<typeof Story>)}
33+
{#snippet template(args, context)}
3434
<Button {...args}>{args.children}</Button>
3535
{/snippet}
3636

examples/Templating.stories.svelte

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
<script module lang="ts">
2-
import { defineMeta, type Args } from '@storybook/addon-svelte-csf';
2+
import { defineMeta } from '@storybook/addon-svelte-csf';
33
import { expect, within } from '@storybook/test';
44
55
/**
@@ -15,13 +15,11 @@
1515
* reference any root-level snippet, for that snippet to be the fallback snippet,
1616
* that is used in any story without explicit template.
1717
*/
18-
//@ts-expect-error TS does not understand that the snippet is defined before this call
19-
2018
render: defaultTemplate,
2119
});
2220
</script>
2321

24-
{#snippet defaultTemplate(args: Args<typeof Story>)}
22+
{#snippet defaultTemplate(args: { text: string })}
2523
<h2 data-testid="heading">Default template</h2>
2624
<p>{args?.text}</p>
2725
{/snippet}
@@ -89,7 +87,7 @@
8987
{/snippet}
9088
</Story>
9189

92-
{#snippet sharedTemplate(args: Args<typeof Story>)}
90+
{#snippet sharedTemplate(args: { text: string })}
9391
<h2 data-testid="heading">Shared template</h2>
9492
<p>{args?.text}</p>
9593
{/snippet}
@@ -101,7 +99,7 @@
10199
Example:
102100
103101
```svelte
104-
{#snippet template(args: Args<typeof Story>)}
102+
{#snippet template(args: { text: string })}
105103
<SomeComponent {...args}>
106104
My custom template to reuse across several stories
107105
</SomeComponent>
@@ -135,15 +133,15 @@
135133
136134
```svelte
137135
<script>
138-
import { defineMeta, type Args } from '@storybook/addon-svelte-csf';
136+
import { defineMeta } from '@storybook/addon-svelte-csf';
139137
140138
const { Story } = defineMeta({
141139
...,
142140
render: defaultTemplate
143141
})
144142
</script>
145143
146-
{#snippet defaultTemplate(args: Args<typeof Story>)}
144+
{#snippet defaultTemplate(args: { text: string })}
147145
<SomeComponent {...args}>
148146
A default template to be used in <Story> components which doesn't have an explicit template set
149147
</SomeComponent>

examples/Types.stories.svelte

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<script lang="ts" module>
2+
import { defineMeta, type StoryContext } from '@storybook/addon-svelte-csf';
3+
import Layout from './components/Layout.svelte';
4+
import type { ComponentProps } from 'svelte';
5+
import type { Merge } from 'type-fest';
6+
const { Story } = defineMeta({
7+
component: Layout,
8+
render: template,
9+
args: {
10+
mainFontSize: 'large',
11+
header: 'default header',
12+
},
13+
argTypes: {
14+
footer: {
15+
control: 'text',
16+
},
17+
children: {
18+
control: 'text',
19+
},
20+
header: {
21+
control: 'text',
22+
},
23+
},
24+
tags: ['autodocs'],
25+
});
26+
27+
type Args = Omit<ComponentProps<typeof Layout>, 'footer' | 'children' | 'header'> & {
28+
footer?: string;
29+
children: string;
30+
header: string;
31+
};
32+
33+
// OR use the Merge helper from the 'type-fest' package:
34+
type SimplerArgs = Merge<
35+
ComponentProps<typeof Layout>,
36+
{
37+
footer?: string;
38+
children: string;
39+
header: string;
40+
}
41+
>;
42+
</script>
43+
44+
{#snippet template({ children, ...args }: Args, context: StoryContext<Args>)}
45+
<Layout {...args}>
46+
{#snippet header()}
47+
{args.header}
48+
{/snippet}
49+
{children}
50+
{#snippet footer()}
51+
{args.footer}
52+
{/snippet}
53+
</Layout>
54+
{/snippet}
55+
56+
<Story name="Default" />
57+
58+
<Story
59+
name="With all args"
60+
args={{
61+
mainFontSize: 'large',
62+
header: 'Header',
63+
footer: 'Footer',
64+
children: 'Children',
65+
emphasizeHeader: true,
66+
}}
67+
/>
68+
69+
<Story name="With mainFontSize" args={{ mainFontSize: 'small' }} />
70+
71+
<Story name="With String Header" args={{ header: 'Header' }} />
72+
73+
<Story name="With static Header and Footer snippets">
74+
{#snippet template({ children, ...args }, context)}
75+
<Layout {...args}>
76+
{#snippet header()}
77+
This is a header
78+
{/snippet}
79+
{children}
80+
{#snippet footer()}
81+
This is a footer
82+
{/snippet}
83+
</Layout>
84+
{/snippet}
85+
</Story>

examples/components/Layout.svelte

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
<script lang="ts">
2+
import type { Snippet } from 'svelte';
3+
4+
type LayoutProps = {
5+
children: Snippet;
6+
header: Snippet;
7+
footer?: Snippet;
8+
mainFontSize: 'small' | 'medium' | 'large';
9+
emphasizeHeader?: boolean;
10+
};
11+
12+
let { children, header, footer, emphasizeHeader = false, mainFontSize }: LayoutProps = $props();
13+
</script>
14+
15+
<div class="layout">
16+
<div class={['header', emphasizeHeader && 'emphasize']}>
17+
{@render header()}
18+
</div>
19+
<main
20+
class={[
21+
'main',
22+
{
23+
'font-small': mainFontSize === 'small',
24+
'font-medium': mainFontSize === 'medium',
25+
'font-large': mainFontSize === 'large',
26+
},
27+
]}
28+
>
29+
{@render children()}
30+
</main>
31+
{#if footer}
32+
<div class="footer">
33+
{@render footer()}
34+
</div>
35+
{/if}
36+
</div>
37+
38+
<style>
39+
.layout {
40+
display: flex;
41+
flex-direction: column;
42+
height: 100%;
43+
}
44+
.layout > * {
45+
padding: 1rem;
46+
}
47+
48+
.header,
49+
.footer {
50+
background-color: palevioletred;
51+
color: white;
52+
}
53+
54+
.header.emphasize {
55+
background-color: red;
56+
}
57+
58+
.main.font-small {
59+
font-size: 12px;
60+
}
61+
62+
.main.font-medium {
63+
font-size: 16px;
64+
}
65+
66+
.main.font-large {
67+
font-size: 20px;
68+
}
69+
</style>

package.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@
7777
"@tsconfig/svelte": "^5.0.4",
7878
"@types/estree": "^1.0.6",
7979
"@types/node": "^20.14.9",
80-
"@vitest/browser": "2.1.4",
81-
"@vitest/coverage-v8": "2.1.4",
82-
"@vitest/ui": "^2.1.4",
80+
"@vitest/browser": "^3.1.3",
81+
"@vitest/coverage-v8": "^3.1.3",
82+
"@vitest/ui": "^3.1.3",
8383
"auto": "^11.1.6",
8484
"chromatic": "^11.28.2",
8585
"concurrently": "^8.2.2",
@@ -94,16 +94,16 @@
9494
"rollup": "^4.25.0",
9595
"storybook": "^9.0.0-0",
9696
"svelte": "^5.28.2",
97-
"svelte-check": "^4.0.5",
97+
"svelte-check": "^4.1.7",
9898
"tslib": "^2.6.3",
9999
"type-fest": "^4.20.1",
100100
"typescript": "^5.5.2",
101101
"typescript-eslint": "^8.30.1",
102102
"typescript-svelte-plugin": "^0.3.42",
103-
"vite": "^5.4.11",
103+
"vite": "^6.3.5",
104104
"vite-plugin-inspect": "^0.8.7",
105105
"vite-plugin-virtual": "^0.3.0",
106-
"vitest": "^2.1.4"
106+
"vitest": "^3.1.3"
107107
},
108108
"peerDependencies": {
109109
"@storybook/svelte": "^0.0.0-0 || ^8.2.0 || ^9.0.0-0",

0 commit comments

Comments
 (0)