Skip to content

Commit

Permalink
fix: rework bindable types strategy (#11420)
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 authored May 2, 2024
1 parent 17b2f62 commit a038d49
Show file tree
Hide file tree
Showing 7 changed files with 34 additions and 120 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];
};
55 changes: 1 addition & 54 deletions packages/svelte/tests/types/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ import {
type ComponentProps,
type ComponentType,
mount,
hydrate,
type Bindable,
type Binding,
type ComponentConstructorOptions
hydrate
} from 'svelte';

SvelteComponent.element === HTMLElement;
Expand Down Expand Up @@ -177,53 +174,3 @@ const x: typeof asLegacyComponent = createClassComponent({
hydrate: true,
component: NewComponent
});

// --------------------------------------------------------------------------- bindable

// Test that
// - everything's bindable unless the component constructor is specifically set telling otherwise (for backwards compatibility)
// - when using mount etc the props are never bindable because this is language-tools only concept

function binding<T>(value: T): Binding<T> {
return value as any;
}

class Explicit extends SvelteComponent<{
foo: string;
bar: Bindable<boolean>;
}> {
constructor(options: ComponentConstructorOptions<{ foo: string; bar: Bindable<boolean> }>) {
super(options);
}
}
new Explicit({ target: null as any, props: { foo: 'foo', bar: binding(true) } });
new Explicit({ target: null as any, props: { foo: 'foo', bar: true } });
new Explicit({
target: null as any,
props: {
// @ts-expect-error
foo: binding(''),
bar: true
}
});
mount(Explicit, { target: null as any, props: { foo: 'foo', bar: true } });
mount(Explicit, {
target: null as any,
props: {
// @ts-expect-error
bar: binding(true)
}
});

class Implicit extends SvelteComponent<{ foo: string; bar: boolean }> {}
new Implicit({ target: null as any, props: { foo: 'foo', bar: true } });
new Implicit({ target: null as any, props: { foo: binding(''), bar: binding(true) } });
mount(Implicit, { target: null as any, props: { foo: 'foo', bar: true } });
mount(Implicit, {
target: null as any,
props: {
foo: 'foo',
// @ts-expect-error
bar: binding(true)
}
});
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 a038d49

Please sign in to comment.