Skip to content

Commit 69c4e3e

Browse files
committed
feat: provide new Component type that represents the shape of components
In Svelte 3 and 4, components were classes under the hood, and the base class was `SvelteComponent`. This class was also used in language tools to properly type check the template code. In Svelte 5, components are functions. To give people a way to extend them programmatically, it would be good to expose the actual shape of components. This is why this PR introduces a new `Component` type. For backwards compatibility reasons, we can't just get rid of the old class-based types. We also need to ensure that language tools can work with both the new and old types: There are many libraries out there that provide `d.ts` files with type definitions written using the class types - these should not error. That's why there's an accompagning language tools PR (sveltejs/language-tools#2380) that's doing the heavy lifting: Instead of generating classes, it now generates a constant and an interfaces and uses Typescript's declaration merging feature to provide both so we can declare a component export as being both a class and a function. That ensures that people can still instantiate them with `new` (which they can do if they use the `legacy.componentApi` compiler option), and it also ensure we don't need to adjust any other code generation mechanisms in language tools yet - from a language tools perspective, classes are still the norm. But through exposing the default export as being _also_ callable as a function we can in a future Svelte version, where classes/the Svelte 4 syntax are removed completely, seamlessly switch over to using functions in the code generation, too, and the `d.ts` files generated up until that point will support it because of the dual shape. This way we have both backwards and forwards compatibility.
1 parent 7dacf2c commit 69c4e3e

File tree

6 files changed

+270
-87
lines changed

6 files changed

+270
-87
lines changed

packages/svelte/src/index.d.ts

Lines changed: 73 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
import './ambient.js';
44

55
/**
6-
* @deprecated Svelte components were classes in Svelte 4. In Svelte 5, thy are not anymore.
6+
* @deprecated Svelte components were classes in Svelte 4. In Svelte 5, they are not anymore.
77
* Use `mount` or `createRoot` instead to instantiate components.
88
* See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes)
99
* for more info.
@@ -34,32 +34,10 @@ type Properties<Props, Slots> = Props &
3434
: {});
3535

3636
/**
37-
* Can be used to create strongly typed Svelte components.
38-
*
39-
* #### Example:
40-
*
41-
* You have component library on npm called `component-library`, from which
42-
* you export a component called `MyComponent`. For Svelte+TypeScript users,
43-
* you want to provide typings. Therefore you create a `index.d.ts`:
44-
* ```ts
45-
* import { SvelteComponent } from "svelte";
46-
* export class MyComponent extends SvelteComponent<{foo: string}> {}
47-
* ```
48-
* Typing this makes it possible for IDEs like VS Code with the Svelte extension
49-
* to provide intellisense and to use the component like this in a Svelte file
50-
* with TypeScript:
51-
* ```svelte
52-
* <script lang="ts">
53-
* import { MyComponent } from "component-library";
54-
* </script>
55-
* <MyComponent foo={'bar'} />
56-
* ```
57-
*
5837
* This was the base class for Svelte components in Svelte 4. Svelte 5+ components
59-
* are completely different under the hood. You should only use this type for typing,
60-
* not actually instantiate components with `new` - use `mount` or `createRoot` instead.
61-
* See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes)
62-
* for more info.
38+
* are completely different under the hood. For typing, use `Component` instead.
39+
* To instantiate components, use `mount` or `createRoot`.
40+
* See [breaking changes documentation](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) for more info.
6341
*/
6442
export class SvelteComponent<
6543
Props extends Record<string, any> = Record<string, any>,
@@ -80,27 +58,25 @@ export class SvelteComponent<
8058
* For type checking capabilities only.
8159
* Does not exist at runtime.
8260
* ### DO NOT USE!
83-
* */
61+
*/
8462
$$prop_def: Props; // Without Properties: unnecessary, causes type bugs
8563
/**
8664
* For type checking capabilities only.
8765
* Does not exist at runtime.
8866
* ### DO NOT USE!
89-
*
90-
* */
67+
*/
9168
$$events_def: Events;
9269
/**
9370
* For type checking capabilities only.
9471
* Does not exist at runtime.
9572
* ### DO NOT USE!
96-
*
97-
* */
73+
*/
9874
$$slot_def: Slots;
9975
/**
10076
* For type checking capabilities only.
10177
* Does not exist at runtime.
10278
* ### DO NOT USE!
103-
* */
79+
*/
10480
$$bindings?: string;
10581

10682
/**
@@ -129,7 +105,61 @@ export class SvelteComponent<
129105
}
130106

131107
/**
132-
* @deprecated Use `SvelteComponent` instead. See TODO for more information.
108+
* Can be used to create strongly typed Svelte components.
109+
*
110+
* #### Example:
111+
*
112+
* You have component library on npm called `component-library`, from which
113+
* you export a component called `MyComponent`. For Svelte+TypeScript users,
114+
* you want to provide typings. Therefore you create a `index.d.ts`:
115+
* ```ts
116+
* import { Component } from "svelte";
117+
* export declare const MyComponent: Component<{ foo: string }> {}
118+
* ```
119+
* Typing this makes it possible for IDEs like VS Code with the Svelte extension
120+
* to provide intellisense and to use the component like this in a Svelte file
121+
* with TypeScript:
122+
* ```svelte
123+
* <script lang="ts">
124+
* import { MyComponent } from "component-library";
125+
* </script>
126+
* <MyComponent foo={'bar'} />
127+
* ```
128+
*/
129+
export interface Component<
130+
Props extends Record<string, any> = {},
131+
Exports extends Record<string, any> = {},
132+
Bindings extends keyof Props | '' = ''
133+
> {
134+
/**
135+
* @param internal An internal object used by Svelte. Do not use or modify.
136+
* @param props The props passed to the component.
137+
*/
138+
(
139+
internal: unknown,
140+
props: Props
141+
): {
142+
/**
143+
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
144+
* is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes
145+
* for more info.
146+
*/
147+
$on?(type: string, callback: (e: any) => void): () => void;
148+
/**
149+
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
150+
* is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes
151+
* for more info.
152+
*/
153+
$set?(props: Partial<Props>): void;
154+
} & Exports;
155+
/** The custom element version of the component. Only present if compiled with the `customElement` compiler option */
156+
element?: typeof HTMLElement;
157+
/** Does not exist at runtime, for typing capabilities only. DO NOT USE */
158+
z_$$bindings?: Bindings;
159+
}
160+
161+
/**
162+
* @deprecated Use `Component` instead. See [breaking changes documentation](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) for more information.
133163
*/
134164
export class SvelteComponentTyped<
135165
Props extends Record<string, any> = Record<string, any>,
@@ -138,6 +168,8 @@ export class SvelteComponentTyped<
138168
> extends SvelteComponent<Props, Events, Slots> {}
139169

140170
/**
171+
* @deprecated The new `Component` type does not have a dedicated Events type. Use `ComponentProps` instead.
172+
*
141173
* Convenience type to get the events the given component expects. Example:
142174
* ```html
143175
* <script lang="ts">
@@ -166,10 +198,16 @@ export type ComponentEvents<Comp extends SvelteComponent> =
166198
* </script>
167199
* ```
168200
*/
169-
export type ComponentProps<Comp extends SvelteComponent> =
170-
Comp extends SvelteComponent<infer Props> ? Props : never;
201+
export type ComponentProps<Comp extends SvelteComponent | Component> =
202+
Comp extends SvelteComponent<infer Props>
203+
? Props
204+
: Comp extends Component<infer Props>
205+
? Props
206+
: never;
171207

172208
/**
209+
* @deprecated This type is obsolete when working with the new `Component` type.
210+
*
173211
* Convenience type to get the type of a Svelte component. Useful for example in combination with
174212
* dynamic components using `<svelte:component>`.
175213
*

packages/svelte/src/internal/client/render.js

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -84,13 +84,12 @@ export function stringify(value) {
8484
*
8585
* @template {Record<string, any>} Props
8686
* @template {Record<string, any>} Exports
87-
* @template {Record<string, any>} Events
88-
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
87+
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props>> | import('../../index.js').Component<Props, Exports, any>} component
8988
* @param {{
9089
* target: Document | Element | ShadowRoot;
9190
* anchor?: Node;
9291
* props?: Props;
93-
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
92+
* events?: Record<string, (e: any) => any>;
9493
* context?: Map<any, any>;
9594
* intro?: boolean;
9695
* }} options
@@ -111,12 +110,11 @@ export function mount(component, options) {
111110
*
112111
* @template {Record<string, any>} Props
113112
* @template {Record<string, any>} Exports
114-
* @template {Record<string, any>} Events
115-
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
113+
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props>> | import('../../index.js').Component<Props, Exports, any>} component
116114
* @param {{
117115
* target: Document | Element | ShadowRoot;
118116
* props?: Props;
119-
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
117+
* events?: Record<string, (e: any) => any>;
120118
* context?: Map<any, any>;
121119
* intro?: boolean;
122120
* recover?: boolean;
@@ -184,7 +182,7 @@ export function hydrate(component, options) {
184182

185183
/**
186184
* @template {Record<string, any>} Exports
187-
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>>} Component
185+
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>> | import('../../index.js').Component<any>} Component
188186
* @param {{
189187
* target: Document | Element | ShadowRoot;
190188
* anchor: Node;

packages/svelte/src/legacy/legacy-client.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { define_property } from '../internal/client/utils.js';
1414
* @template {Record<string, any>} Slots
1515
*
1616
* @param {import('svelte').ComponentConstructorOptions<Props> & {
17-
* component: import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots>>;
17+
* component: import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots>> | import('svelte').Component<Props>;
1818
* immutable?: boolean;
1919
* hydrate?: boolean;
2020
* recover?: boolean;
@@ -36,7 +36,7 @@ export function createClassComponent(options) {
3636
* @template {Record<string, any>} Events
3737
* @template {Record<string, any>} Slots
3838
*
39-
* @param {import('svelte').SvelteComponent<Props, Events, Slots>} component
39+
* @param {import('svelte').SvelteComponent<Props, Events, Slots> | import('svelte').Component<Props>} component
4040
* @returns {import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots> & Exports>}
4141
*/
4242
export function asClassComponent(component) {

packages/svelte/tests/types/component.ts

Lines changed: 82 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@ import {
55
type ComponentProps,
66
type ComponentType,
77
mount,
8-
hydrate
8+
hydrate,
9+
type Component
910
} from 'svelte';
1011

1112
SvelteComponent.element === HTMLElement;
@@ -49,6 +50,15 @@ const legacyComponentEvents2: ComponentEvents<LegacyComponent> = {
4950
event: new KeyboardEvent('click')
5051
};
5152

53+
const legacyComponentInstance: SvelteComponent<{ prop: string }> = new LegacyComponent({
54+
target: null as any as Document | Element | ShadowRoot,
55+
props: {
56+
prop: 'foo'
57+
}
58+
});
59+
60+
const legacyComponentClass: typeof SvelteComponent<{ prop: string }> = LegacyComponent;
61+
5262
// --------------------------------------------------------------------------- new: functions
5363

5464
class NewComponent extends SvelteComponent<
@@ -130,7 +140,7 @@ hydrate(NewComponent, {
130140
},
131141
events: {
132142
event: (e) =>
133-
// @ts-expect-error
143+
// we're not type checking this as it's an edge case and removing the generic later would be an annoying mini breaking change
134144
e.doesNotExist
135145
},
136146
immutable: true,
@@ -174,3 +184,73 @@ const x: typeof asLegacyComponent = createClassComponent({
174184
hydrate: true,
175185
component: NewComponent
176186
});
187+
188+
// --------------------------------------------------------------------------- function component
189+
190+
const functionComponent: Component<
191+
{ binding: boolean; readonly: string },
192+
{ foo: 'bar' },
193+
'binding'
194+
> = (a, props) => {
195+
props.binding === true;
196+
props.readonly === 'foo';
197+
// @ts-expect-error
198+
props.readonly = true;
199+
// @ts-expect-error
200+
props.binding = '';
201+
return {
202+
foo: 'bar'
203+
};
204+
};
205+
functionComponent.element === HTMLElement;
206+
207+
functionComponent(null as any, {
208+
binding: true,
209+
// @ts-expect-error
210+
readonly: true
211+
});
212+
213+
const functionComponentInstance = functionComponent(null as any, {
214+
binding: true,
215+
readonly: 'foo',
216+
// @ts-expect-error
217+
x: ''
218+
});
219+
functionComponentInstance.foo === 'bar';
220+
// @ts-expect-error
221+
functionComponentInstance.foo = 'foo';
222+
223+
mount(functionComponent, {
224+
target: null as any as Document | Element | ShadowRoot,
225+
props: {
226+
binding: true,
227+
readonly: 'foo',
228+
// would be nice to error here, probably needs NoInfer type helper in upcoming TS 5.5
229+
x: ''
230+
}
231+
});
232+
mount(functionComponent, {
233+
target: null as any as Document | Element | ShadowRoot,
234+
props: {
235+
binding: true,
236+
// @ts-expect-error wrong type
237+
readonly: 1
238+
}
239+
});
240+
241+
hydrate(functionComponent, {
242+
target: null as any as Document | Element | ShadowRoot,
243+
props: {
244+
binding: true,
245+
readonly: 'foo',
246+
// would be nice to error here, probably needs NoInfer type helper in upcoming TS 5.5
247+
x: ''
248+
}
249+
});
250+
hydrate(functionComponent, {
251+
target: null as any as Document | Element | ShadowRoot,
252+
// @ts-expect-error missing prop
253+
props: {
254+
binding: true
255+
}
256+
});

0 commit comments

Comments
 (0)