fix: rework bindable types strategy (#11420)

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
pull/11410/head
Simon H 5 months ago committed by GitHub
parent 17b2f6215d
commit a038d49f78
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,5 @@
---
"svelte": patch
---
fix: rework binding type-checking strategy

@ -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.
@ -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
@ -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.
@ -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
@ -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

@ -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;
@ -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;

@ -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';

@ -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];
};

@ -5,10 +5,7 @@ import {
type ComponentProps,
type ComponentType,
mount,
hydrate,
type Bindable,
type Binding,
type ComponentConstructorOptions
hydrate
} from 'svelte';
SvelteComponent.element === HTMLElement;
@ -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)
}
});

@ -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
@ -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.
@ -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
@ -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
@ -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;
@ -323,6 +304,8 @@ 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
*
@ -330,7 +313,7 @@ declare module 'svelte' {
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;
@ -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;

Loading…
Cancel
Save