Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/heavy-doors-applaud.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: provide `Component` type that represents the new shape of Svelte components
108 changes: 73 additions & 35 deletions packages/svelte/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -34,32 +34,10 @@ type Properties<Props, Slots> = 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
* <script lang="ts">
* import { MyComponent } from "component-library";
* </script>
* <MyComponent foo={'bar'} />
* ```
*
* 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<string, any> = Record<string, any>,
Expand All @@ -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;

/**
Expand Down Expand Up @@ -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
* <script lang="ts">
* import { MyComponent } from "component-library";
* </script>
* <MyComponent foo={'bar'} />
* ```
*/
export interface Component<
Props extends Record<string, any> = {},
Exports extends Record<string, any> = {},
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<Props>): 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;
Comment on lines +157 to +158
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we just use @deprecated to make sure this sinks to the bottom, rather than the z_ prefix?

Suggested change
/** Does not exist at runtime, for typing capabilities only. DO NOT USE */
z_$$bindings?: Bindings;
/** @deprecated Does not exist at runtime, for typing capabilities only. DO NOT USE */
$$bindings?: Bindings;

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I prefer we don't because using it would print out "this is deprecated" warnings from svelte-check from the generated locations, and filtering those out is a bit tough. I'm also not sure if all IDEs move it to the bottom. Lastly, this is a very rare thing to show up anyway because it's on the function object, not the instance type.

}

/**
* @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<string, any> = Record<string, any>,
Expand All @@ -138,6 +168,8 @@ export class SvelteComponentTyped<
> extends SvelteComponent<Props, Events, Slots> {}

/**
* @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
* <script lang="ts">
Expand Down Expand Up @@ -166,10 +198,16 @@ export type ComponentEvents<Comp extends SvelteComponent> =
* </script>
* ```
*/
export type ComponentProps<Comp extends SvelteComponent> =
Comp extends SvelteComponent<infer Props> ? Props : never;
export type ComponentProps<Comp extends SvelteComponent | Component> =
Comp extends SvelteComponent<infer Props>
? Props
: Comp extends Component<infer Props>
? 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 `<svelte:component>`.
*
Expand Down
12 changes: 5 additions & 7 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,13 +84,12 @@ export function stringify(value) {
*
* @template {Record<string, any>} Props
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props>> | import('../../index.js').Component<Props, Exports, any>} component
* @param {{
* target: Document | Element | ShadowRoot;
* anchor?: Node;
* props?: Props;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* events?: Record<string, (e: any) => any>;
* context?: Map<any, any>;
* intro?: boolean;
* }} options
Expand All @@ -111,12 +110,11 @@ export function mount(component, options) {
*
* @template {Record<string, any>} Props
* @template {Record<string, any>} Exports
* @template {Record<string, any>} Events
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props>> | import('../../index.js').Component<Props, Exports, any>} component
* @param {{
* target: Document | Element | ShadowRoot;
* props?: Props;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* events?: Record<string, (e: any) => any>;
* context?: Map<any, any>;
* intro?: boolean;
* recover?: boolean;
Expand Down Expand Up @@ -184,7 +182,7 @@ export function hydrate(component, options) {

/**
* @template {Record<string, any>} Exports
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>>} Component
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<any>> | import('../../index.js').Component<any>} Component
* @param {{
* target: Document | Element | ShadowRoot;
* anchor: Node;
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/legacy/legacy-client.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { define_property } from '../internal/client/utils.js';
* @template {Record<string, any>} Slots
*
* @param {import('svelte').ComponentConstructorOptions<Props> & {
* component: import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots>>;
* component: import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots>> | import('svelte').Component<Props>;
* immutable?: boolean;
* hydrate?: boolean;
* recover?: boolean;
Expand All @@ -36,7 +36,7 @@ export function createClassComponent(options) {
* @template {Record<string, any>} Events
* @template {Record<string, any>} Slots
*
* @param {import('svelte').SvelteComponent<Props, Events, Slots>} component
* @param {import('svelte').SvelteComponent<Props, Events, Slots> | import('svelte').Component<Props>} component
* @returns {import('svelte').ComponentType<import('svelte').SvelteComponent<Props, Events, Slots> & Exports>}
*/
export function asClassComponent(component) {
Expand Down
84 changes: 82 additions & 2 deletions packages/svelte/tests/types/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
type ComponentProps,
type ComponentType,
mount,
hydrate
hydrate,
type Component
} from 'svelte';

SvelteComponent.element === HTMLElement;
Expand Down Expand Up @@ -49,6 +50,15 @@ const legacyComponentEvents2: ComponentEvents<LegacyComponent> = {
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<
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}
});
Loading