diff --git a/.changeset/heavy-doors-applaud.md b/.changeset/heavy-doors-applaud.md new file mode 100644 index 000000000000..99623344d4cb --- /dev/null +++ b/.changeset/heavy-doors-applaud.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: provide `Component` type that represents the new shape of Svelte components diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index 956e97ed8f88..71c180d52232 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -3,7 +3,7 @@ import './ambient.js'; /** - * @deprecated Svelte components were classes in Svelte 4. In Svelte 5, thy are not anymore. + * @deprecated In Svelte 4, components are classes. In Svelte 5, they are functions. * Use `mount` or `createRoot` instead to instantiate components. * See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) * for more info. @@ -34,32 +34,10 @@ type Properties = Props & : {}); /** - * Can be used to create strongly typed Svelte components. - * - * #### Example: - * - * You have component library on npm called `component-library`, from which - * you export a component called `MyComponent`. For Svelte+TypeScript users, - * you want to provide typings. Therefore you create a `index.d.ts`: - * ```ts - * import { SvelteComponent } from "svelte"; - * export class MyComponent extends SvelteComponent<{foo: string}> {} - * ``` - * Typing this makes it possible for IDEs like VS Code with the Svelte extension - * to provide intellisense and to use the component like this in a Svelte file - * with TypeScript: - * ```svelte - * - * - * ``` - * * This was the base class for Svelte components in Svelte 4. Svelte 5+ components - * are completely different under the hood. You should only use this type for typing, - * not actually instantiate components with `new` - use `mount` or `createRoot` instead. - * See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) - * for more info. + * are completely different under the hood. For typing, use `Component` instead. + * To instantiate components, use `mount` or `createRoot`. + * See [breaking changes documentation](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) for more info. */ export class SvelteComponent< Props extends Record = Record, @@ -80,27 +58,25 @@ export class SvelteComponent< * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * */ + */ $$prop_def: Props; // Without Properties: unnecessary, causes type bugs /** * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * - * */ + */ $$events_def: Events; /** * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * - * */ + */ $$slot_def: Slots; /** * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * */ + */ $$bindings?: string; /** @@ -129,7 +105,61 @@ export class SvelteComponent< } /** - * @deprecated Use `SvelteComponent` instead. See TODO for more information. + * Can be used to create strongly typed Svelte components. + * + * #### Example: + * + * You have component library on npm called `component-library`, from which + * you export a component called `MyComponent`. For Svelte+TypeScript users, + * you want to provide typings. Therefore you create a `index.d.ts`: + * ```ts + * import { Component } from "svelte"; + * export declare const MyComponent: Component<{ foo: string }> {} + * ``` + * Typing this makes it possible for IDEs like VS Code with the Svelte extension + * to provide intellisense and to use the component like this in a Svelte file + * with TypeScript: + * ```svelte + * + * + * ``` + */ +export interface Component< + Props extends Record = {}, + Exports extends Record = {}, + Bindings extends keyof Props | '' = '' +> { + /** + * @param internal An internal object used by Svelte. Do not use or modify. + * @param props The props passed to the component. + */ + ( + internal: unknown, + props: Props + ): { + /** + * @deprecated This method only exists when using one of the legacy compatibility helpers, which + * is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes + * for more info. + */ + $on?(type: string, callback: (e: any) => void): () => void; + /** + * @deprecated This method only exists when using one of the legacy compatibility helpers, which + * is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes + * for more info. + */ + $set?(props: Partial): void; + } & Exports; + /** The custom element version of the component. Only present if compiled with the `customElement` compiler option */ + element?: typeof HTMLElement; + /** Does not exist at runtime, for typing capabilities only. DO NOT USE */ + z_$$bindings?: Bindings; +} + +/** + * @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. */ export class SvelteComponentTyped< Props extends Record = Record, @@ -138,6 +168,8 @@ export class SvelteComponentTyped< > extends SvelteComponent {} /** + * @deprecated The new `Component` type does not have a dedicated Events type. Use `ComponentProps` instead. + * * Convenience type to get the events the given component expects. Example: * ```html * * ``` */ -export type ComponentProps = - Comp extends SvelteComponent ? Props : never; +export type ComponentProps = + Comp extends SvelteComponent + ? Props + : Comp extends Component + ? Props + : never; /** + * @deprecated This type is obsolete when working with the new `Component` type. + * * Convenience type to get the type of a Svelte component. Useful for example in combination with * dynamic components using ``. * diff --git a/packages/svelte/src/internal/client/render.js b/packages/svelte/src/internal/client/render.js index 101ba6f1afd3..dba3819033b4 100644 --- a/packages/svelte/src/internal/client/render.js +++ b/packages/svelte/src/internal/client/render.js @@ -84,13 +84,12 @@ export function stringify(value) { * * @template {Record} Props * @template {Record} Exports - * @template {Record} Events - * @param {import('../../index.js').ComponentType>} component + * @param {import('../../index.js').ComponentType> | import('../../index.js').Component} component * @param {{ * target: Document | Element | ShadowRoot; * anchor?: Node; * props?: Props; - * events?: { [Property in keyof Events]: (e: Events[Property]) => any }; + * events?: Record any>; * context?: Map; * intro?: boolean; * }} options @@ -111,12 +110,11 @@ export function mount(component, options) { * * @template {Record} Props * @template {Record} Exports - * @template {Record} Events - * @param {import('../../index.js').ComponentType>} component + * @param {import('../../index.js').ComponentType> | import('../../index.js').Component} component * @param {{ * target: Document | Element | ShadowRoot; * props?: Props; - * events?: { [Property in keyof Events]: (e: Events[Property]) => any }; + * events?: Record any>; * context?: Map; * intro?: boolean; * recover?: boolean; @@ -181,7 +179,7 @@ export function hydrate(component, options) { /** * @template {Record} Exports - * @param {import('../../index.js').ComponentType>} Component + * @param {import('../../index.js').ComponentType> | import('../../index.js').Component} Component * @param {{ * target: Document | Element | ShadowRoot; * anchor: Node; diff --git a/packages/svelte/src/legacy/legacy-client.js b/packages/svelte/src/legacy/legacy-client.js index 8784489c4d48..e4d4122d89f0 100644 --- a/packages/svelte/src/legacy/legacy-client.js +++ b/packages/svelte/src/legacy/legacy-client.js @@ -14,7 +14,7 @@ import { define_property } from '../internal/client/utils.js'; * @template {Record} Slots * * @param {import('svelte').ComponentConstructorOptions & { - * component: import('svelte').ComponentType>; + * component: import('svelte').ComponentType> | import('svelte').Component; * immutable?: boolean; * hydrate?: boolean; * recover?: boolean; @@ -36,7 +36,7 @@ export function createClassComponent(options) { * @template {Record} Events * @template {Record} Slots * - * @param {import('svelte').SvelteComponent} component + * @param {import('svelte').SvelteComponent | import('svelte').Component} component * @returns {import('svelte').ComponentType & Exports>} */ export function asClassComponent(component) { diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 55477441bb17..b240d4a4bee3 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -5,7 +5,8 @@ import { type ComponentProps, type ComponentType, mount, - hydrate + hydrate, + type Component } from 'svelte'; SvelteComponent.element === HTMLElement; @@ -49,6 +50,15 @@ const legacyComponentEvents2: ComponentEvents = { event: new KeyboardEvent('click') }; +const legacyComponentInstance: SvelteComponent<{ prop: string }> = new LegacyComponent({ + target: null as any as Document | Element | ShadowRoot, + props: { + prop: 'foo' + } +}); + +const legacyComponentClass: typeof SvelteComponent<{ prop: string }> = LegacyComponent; + // --------------------------------------------------------------------------- new: functions class NewComponent extends SvelteComponent< @@ -130,7 +140,7 @@ hydrate(NewComponent, { }, events: { event: (e) => - // @ts-expect-error + // we're not type checking this as it's an edge case and removing the generic later would be an annoying mini breaking change e.doesNotExist }, immutable: true, @@ -174,3 +184,73 @@ const x: typeof asLegacyComponent = createClassComponent({ hydrate: true, component: NewComponent }); + +// --------------------------------------------------------------------------- function component + +const functionComponent: Component< + { binding: boolean; readonly: string }, + { foo: 'bar' }, + 'binding' +> = (a, props) => { + props.binding === true; + props.readonly === 'foo'; + // @ts-expect-error + props.readonly = true; + // @ts-expect-error + props.binding = ''; + return { + foo: 'bar' + }; +}; +functionComponent.element === HTMLElement; + +functionComponent(null as any, { + binding: true, + // @ts-expect-error + readonly: true +}); + +const functionComponentInstance = functionComponent(null as any, { + binding: true, + readonly: 'foo', + // @ts-expect-error + x: '' +}); +functionComponentInstance.foo === 'bar'; +// @ts-expect-error +functionComponentInstance.foo = 'foo'; + +mount(functionComponent, { + target: null as any as Document | Element | ShadowRoot, + props: { + binding: true, + readonly: 'foo', + // would be nice to error here, probably needs NoInfer type helper in upcoming TS 5.5 + x: '' + } +}); +mount(functionComponent, { + target: null as any as Document | Element | ShadowRoot, + props: { + binding: true, + // @ts-expect-error wrong type + readonly: 1 + } +}); + +hydrate(functionComponent, { + target: null as any as Document | Element | ShadowRoot, + props: { + binding: true, + readonly: 'foo', + // would be nice to error here, probably needs NoInfer type helper in upcoming TS 5.5 + x: '' + } +}); +hydrate(functionComponent, { + target: null as any as Document | Element | ShadowRoot, + // @ts-expect-error missing prop + props: { + binding: true + } +}); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index a90f0e40e7b5..48827bfc7e00 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -1,6 +1,6 @@ declare module 'svelte' { /** - * @deprecated Svelte components were classes in Svelte 4. In Svelte 5, thy are not anymore. + * @deprecated In Svelte 4, components are classes. In Svelte 5, they are functions. * Use `mount` or `createRoot` instead to instantiate components. * See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) * for more info. @@ -31,32 +31,10 @@ declare module 'svelte' { : {}); /** - * Can be used to create strongly typed Svelte components. - * - * #### Example: - * - * You have component library on npm called `component-library`, from which - * you export a component called `MyComponent`. For Svelte+TypeScript users, - * you want to provide typings. Therefore you create a `index.d.ts`: - * ```ts - * import { SvelteComponent } from "svelte"; - * export class MyComponent extends SvelteComponent<{foo: string}> {} - * ``` - * Typing this makes it possible for IDEs like VS Code with the Svelte extension - * to provide intellisense and to use the component like this in a Svelte file - * with TypeScript: - * ```svelte - * - * - * ``` - * * This was the base class for Svelte components in Svelte 4. Svelte 5+ components - * are completely different under the hood. You should only use this type for typing, - * not actually instantiate components with `new` - use `mount` or `createRoot` instead. - * See [breaking changes](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) - * for more info. + * are completely different under the hood. For typing, use `Component` instead. + * To instantiate components, use `mount` or `createRoot`. + * See [breaking changes documentation](https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes) for more info. */ export class SvelteComponent< Props extends Record = Record, @@ -77,27 +55,25 @@ declare module 'svelte' { * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * */ + */ $$prop_def: Props; // Without Properties: unnecessary, causes type bugs /** * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * - * */ + */ $$events_def: Events; /** * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * - * */ + */ $$slot_def: Slots; /** * For type checking capabilities only. * Does not exist at runtime. * ### DO NOT USE! - * */ + */ $$bindings?: string; /** @@ -126,7 +102,61 @@ declare module 'svelte' { } /** - * @deprecated Use `SvelteComponent` instead. See TODO for more information. + * Can be used to create strongly typed Svelte components. + * + * #### Example: + * + * You have component library on npm called `component-library`, from which + * you export a component called `MyComponent`. For Svelte+TypeScript users, + * you want to provide typings. Therefore you create a `index.d.ts`: + * ```ts + * import { Component } from "svelte"; + * export declare const MyComponent: Component<{ foo: string }> {} + * ``` + * Typing this makes it possible for IDEs like VS Code with the Svelte extension + * to provide intellisense and to use the component like this in a Svelte file + * with TypeScript: + * ```svelte + * + * + * ``` + */ + export interface Component< + Props extends Record = {}, + Exports extends Record = {}, + Bindings extends keyof Props | '' = '' + > { + /** + * @param internal An internal object used by Svelte. Do not use or modify. + * @param props The props passed to the component. + */ + ( + internal: unknown, + props: Props + ): { + /** + * @deprecated This method only exists when using one of the legacy compatibility helpers, which + * is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes + * for more info. + */ + $on?(type: string, callback: (e: any) => void): () => void; + /** + * @deprecated This method only exists when using one of the legacy compatibility helpers, which + * is a stop-gap solution. See https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes + * for more info. + */ + $set?(props: Partial): void; + } & Exports; + /** The custom element version of the component. Only present if compiled with the `customElement` compiler option */ + element?: typeof HTMLElement; + /** Does not exist at runtime, for typing capabilities only. DO NOT USE */ + z_$$bindings?: Bindings; + } + + /** + * @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. */ export class SvelteComponentTyped< Props extends Record = Record, @@ -135,6 +165,8 @@ declare module 'svelte' { > extends SvelteComponent {} /** + * @deprecated The new `Component` type does not have a dedicated Events type. Use `ComponentProps` instead. + * * Convenience type to get the events the given component expects. Example: * ```html * * ``` */ - export type ComponentProps = - Comp extends SvelteComponent ? Props : never; + export type ComponentProps = + Comp extends SvelteComponent + ? Props + : Comp extends Component + ? Props + : never; /** + * @deprecated This type is obsolete when working with the new `Component` type. + * * Convenience type to get the type of a Svelte component. Useful for example in combination with * dynamic components using ``. * @@ -310,11 +348,11 @@ declare module 'svelte' { * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ - export function mount, Exports extends Record, Events extends Record>(component: ComponentType>, options: { + export function mount, Exports extends Record>(component: ComponentType> | Component, options: { target: Document | Element | ShadowRoot; anchor?: Node | undefined; props?: Props | undefined; - events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined; + events?: Record any> | undefined; context?: Map | undefined; intro?: boolean | undefined; }): Exports; @@ -322,10 +360,10 @@ declare module 'svelte' { * Hydrates a component on the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component * * */ - export function hydrate, Exports extends Record, Events extends Record>(component: ComponentType>, options: { + export function hydrate, Exports extends Record>(component: ComponentType> | Component, options: { target: Document | Element | ShadowRoot; props?: Props | undefined; - events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined; + events?: Record any> | undefined; context?: Map | undefined; intro?: boolean | undefined; recover?: boolean | undefined; @@ -1952,7 +1990,7 @@ declare module 'svelte/legacy' { * * */ export function createClassComponent, Exports extends Record, Events extends Record, Slots extends Record>(options: import("svelte").ComponentConstructorOptions & { - component: import("svelte").ComponentType>; + component: import("svelte").ComponentType> | import("svelte").Component; immutable?: boolean | undefined; hydrate?: boolean | undefined; recover?: boolean | undefined; @@ -1963,7 +2001,7 @@ declare module 'svelte/legacy' { * @deprecated Use this only as a temporary solution to migrate your imperative component code to Svelte 5. * * */ - export function asClassComponent, Exports extends Record, Events extends Record, Slots extends Record>(component: import("svelte").SvelteComponent): import("svelte").ComponentType & Exports>; + export function asClassComponent, Exports extends Record, Events extends Record, Slots extends Record>(component: import("svelte").SvelteComponent | import("svelte").Component): import("svelte").ComponentType & Exports>; /** * Runs the given function once immediately on the server, and works like `$effect.pre` on the client. * diff --git a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md index 1a5d571c1e6f..2501ae0ff740 100644 --- a/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md +++ b/sites/svelte-5-preview/src/routes/docs/content/03-appendix/02-breaking-changes.md @@ -86,6 +86,37 @@ import App from './App.svelte'; `render` also no longer returns CSS; it should be served separately from a CSS file. +### Component typing changes + +The change from classes towards functions is also reflected in the typings: `SvelteComponent`, the base class from Svelte 4, is deprecated in favor of the new `Component` type which defines the function shape of a Svelte component. To manually define a component shape in a `d.ts` file: + +```ts +import type { Component } from 'svelte'; +export declare const MyComponent: Component<{ + foo: string; +}>; +``` + +To declare that a component of a certain type is required: + +```svelte + + + +``` + +The two utility types `ComponentEvents` and `ComponentType` are also deprecated. `ComponentEvents` is obsolete because events are defined as callback props now, and `ComponentType` is obsolete because the new `Component` type is the component type already (e.g. `ComponentType>` == `Component<{ prop: string }>`). + ### bind:this changes Because components are no longer classes, using `bind:this` no longer returns a class instance with `$set`, `$on` and `$destroy` methods on it. It only returns the instance exports (`export function/const`) and, if you're using the `accessors` option, a getter/setter-pair for each property.