Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: provide new Component type that represents the shape of components #11775

Merged
merged 3 commits into from
May 28, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 Svelte components were classes in Svelte 4. In Svelte 5, they are not anymore.
dummdidumm marked this conversation as resolved.
Show resolved Hide resolved
* 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
Loading