Skip to content

Commit

Permalink
fix: rework bindable types strategy
Browse files Browse the repository at this point in the history
Instead of using types that declare whether or not a type is bindable directly as part of the property, we're introducing a new for-types-only field to `SvelteComponent`: `$$bindings`, which is typed as the keys of the properties that are bindable (string by default, i.e. everything's bindable; for backwards compat). language-tools can then produce code that assigns to this property which results in an error we can display if the binding is invalid
closes #11356
  • Loading branch information
dummdidumm committed May 1, 2024
1 parent 8b1a269 commit 3d6d823
Show file tree
Hide file tree
Showing 6 changed files with 33 additions and 66 deletions.
5 changes: 5 additions & 0 deletions .changeset/yellow-pugs-raise.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"svelte": patch
---

fix: rework binding type-checking strategy
36 changes: 11 additions & 25 deletions packages/svelte/src/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are).

import './ambient.js';
import type { RemoveBindable } from './internal/types.js';

/**
* @deprecated Svelte components were classes in Svelte 4. In Svelte 5, thy are not anymore.
Expand All @@ -21,29 +20,10 @@ export interface ComponentConstructorOptions<
$$inline?: boolean;
}

/** Tooling for types uses this for properties are being used with `bind:` */
export type Binding<T> = { 'bind:': T };
/**
* Tooling for types uses this for properties that may be bound to.
* Only use this if you author Svelte component type definition files by hand (we recommend using `@sveltejs/package` instead).
* Example:
* ```ts
* export class MyComponent extends SvelteComponent<{ readonly: string, bindable: Bindable<string> }> {}
* ```
* means you can now do `<MyComponent {readonly} bind:bindable />`
* Utility type for ensuring backwards compatibility on a type level that if there's a default slot, add 'children' to the props
*/
export type Bindable<T> = T | Binding<T>;

type WithBindings<T> = {
[Key in keyof T]: Bindable<T[Key]>;
};

/**
* Utility type for ensuring backwards compatibility on a type level:
* - If there's a default slot, add 'children' to the props
* - All props are bindable
*/
type PropsWithChildren<Props, Slots> = WithBindings<Props> &
type Properties<Props, Slots> = Props &
(Slots extends { default: any }
? // This is unfortunate because it means "accepts no props" turns into "accepts any prop"
// but the alternative is non-fixable type errors because of the way TypeScript index
Expand Down Expand Up @@ -95,13 +75,13 @@ export class SvelteComponent<
* is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See
* https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info.
*/
constructor(options: ComponentConstructorOptions<PropsWithChildren<Props, Slots>>);
constructor(options: ComponentConstructorOptions<Properties<Props, Slots>>);
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$prop_def: RemoveBindable<Props>; // Without PropsWithChildren: unnecessary, causes type bugs
$$prop_def: Props; // Without Properties: unnecessary, causes type bugs
/**
* For type checking capabilities only.
* Does not exist at runtime.
Expand All @@ -116,6 +96,12 @@ export class SvelteComponent<
*
* */
$$slot_def: Slots;
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$bindings?: string;

/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
Expand Down Expand Up @@ -181,7 +167,7 @@ export type ComponentEvents<Comp extends SvelteComponent> =
* ```
*/
export type ComponentProps<Comp extends SvelteComponent> =
Comp extends SvelteComponent<infer Props> ? RemoveBindable<Props> : never;
Comp extends SvelteComponent<infer Props> ? Props : never;

/**
* Convenience type to get the type of a Svelte component. Useful for example in combination with
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/internal/client/render.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,7 +95,7 @@ export function stringify(value) {
* @param {{
* target: Document | Element | ShadowRoot;
* anchor?: Node;
* props?: import('../types.js').RemoveBindable<Props>;
* props?: Props;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>;
* intro?: boolean;
Expand All @@ -121,7 +121,7 @@ export function mount(component, options) {
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
* @param {{
* target: Document | Element | ShadowRoot;
* props?: import('../types.js').RemoveBindable<Props>;
* props?: Props;
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
* context?: Map<any, any>;
* intro?: boolean;
Expand Down
1 change: 0 additions & 1 deletion packages/svelte/src/internal/client/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import type { Bindable, Binding } from '../../index.js';
import type { Store } from '#shared';
import { STATE_SYMBOL } from './constants.js';
import type { Effect, Source, Value } from './reactivity/types.js';
Expand Down
6 changes: 0 additions & 6 deletions packages/svelte/src/internal/types.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,2 @@
import type { Bindable } from '../index.js';

/** Anything except a function */
export type NotFunction<T> = T extends Function ? never : T;

export type RemoveBindable<Props extends Record<string, any>> = {
[Key in keyof Props]: Props[Key] extends Bindable<infer Value> ? Value : Props[Key];
};
47 changes: 15 additions & 32 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,29 +17,10 @@ declare module 'svelte' {
$$inline?: boolean;
}

/** Tooling for types uses this for properties are being used with `bind:` */
export type Binding<T> = { 'bind:': T };
/**
* Tooling for types uses this for properties that may be bound to.
* Only use this if you author Svelte component type definition files by hand (we recommend using `@sveltejs/package` instead).
* Example:
* ```ts
* export class MyComponent extends SvelteComponent<{ readonly: string, bindable: Bindable<string> }> {}
* ```
* means you can now do `<MyComponent {readonly} bind:bindable />`
*/
export type Bindable<T> = T | Binding<T>;

type WithBindings<T> = {
[Key in keyof T]: Bindable<T[Key]>;
};

/**
* Utility type for ensuring backwards compatibility on a type level:
* - If there's a default slot, add 'children' to the props
* - All props are bindable
* Utility type for ensuring backwards compatibility on a type level that if there's a default slot, add 'children' to the props
*/
type PropsWithChildren<Props, Slots> = WithBindings<Props> &
type Properties<Props, Slots> = Props &
(Slots extends { default: any }
? // This is unfortunate because it means "accepts no props" turns into "accepts any prop"
// but the alternative is non-fixable type errors because of the way TypeScript index
Expand Down Expand Up @@ -91,13 +72,13 @@ declare module 'svelte' {
* is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See
* https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info.
*/
constructor(options: ComponentConstructorOptions<PropsWithChildren<Props, Slots>>);
constructor(options: ComponentConstructorOptions<Properties<Props, Slots>>);
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$prop_def: RemoveBindable<Props>; // Without PropsWithChildren: unnecessary, causes type bugs
$$prop_def: Props; // Without Properties: unnecessary, causes type bugs
/**
* For type checking capabilities only.
* Does not exist at runtime.
Expand All @@ -112,6 +93,12 @@ declare module 'svelte' {
*
* */
$$slot_def: Slots;
/**
* For type checking capabilities only.
* Does not exist at runtime.
* ### DO NOT USE!
* */
$$bindings?: string;

/**
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
Expand Down Expand Up @@ -177,7 +164,7 @@ declare module 'svelte' {
* ```
*/
export type ComponentProps<Comp extends SvelteComponent> =
Comp extends SvelteComponent<infer Props> ? RemoveBindable<Props> : never;
Comp extends SvelteComponent<infer Props> ? Props : never;

/**
* Convenience type to get the type of a Svelte component. Useful for example in combination with
Expand Down Expand Up @@ -247,12 +234,6 @@ declare module 'svelte' {
: [type: Type, parameter: EventMap[Type], options?: DispatchOptions]
): boolean;
}
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;

type RemoveBindable<Props extends Record<string, any>> = {
[Key in keyof Props]: Props[Key] extends Bindable<infer Value> ? Value : Props[Key];
};
/**
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
* It must be called during the component's initialisation (but doesn't need to live *inside* the component;
Expand Down Expand Up @@ -323,14 +304,16 @@ declare module 'svelte' {
* Synchronously flushes any pending state changes and those that result from it.
* */
export function flushSync(fn?: (() => void) | undefined): void;
/** Anything except a function */
type NotFunction<T> = T extends Function ? never : T;
/**
* 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<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: {
target: Document | Element | ShadowRoot;
anchor?: Node | undefined;
props?: RemoveBindable<Props> | undefined;
props?: Props | undefined;
events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined;
context?: Map<any, any> | undefined;
intro?: boolean | undefined;
Expand All @@ -341,7 +324,7 @@ declare module 'svelte' {
* */
export function hydrate<Props extends Record<string, any>, Exports extends Record<string, any>, Events extends Record<string, any>>(component: ComponentType<SvelteComponent<Props, Events, any>>, options: {
target: Document | Element | ShadowRoot;
props?: RemoveBindable<Props> | undefined;
props?: Props | undefined;
events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined;
context?: Map<any, any> | undefined;
intro?: boolean | undefined;
Expand Down

0 comments on commit 3d6d823

Please sign in to comment.