Skip to content

Commit a038d49

Browse files
authored
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
1 parent 17b2f62 commit a038d49

File tree

7 files changed

+34
-120
lines changed

7 files changed

+34
-120
lines changed

.changeset/yellow-pugs-raise.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"svelte": patch
3+
---
4+
5+
fix: rework binding type-checking strategy

packages/svelte/src/index.d.ts

Lines changed: 11 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// This should contain all the public interfaces (not all of them are actually importable, check current Svelte for which ones are).
22

33
import './ambient.js';
4-
import type { RemoveBindable } from './internal/types.js';
54

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

24-
/** Tooling for types uses this for properties are being used with `bind:` */
25-
export type Binding<T> = { 'bind:': T };
2623
/**
27-
* Tooling for types uses this for properties that may be bound to.
28-
* Only use this if you author Svelte component type definition files by hand (we recommend using `@sveltejs/package` instead).
29-
* Example:
30-
* ```ts
31-
* export class MyComponent extends SvelteComponent<{ readonly: string, bindable: Bindable<string> }> {}
32-
* ```
33-
* means you can now do `<MyComponent {readonly} bind:bindable />`
24+
* Utility type for ensuring backwards compatibility on a type level that if there's a default slot, add 'children' to the props
3425
*/
35-
export type Bindable<T> = T | Binding<T>;
36-
37-
type WithBindings<T> = {
38-
[Key in keyof T]: Bindable<T[Key]>;
39-
};
40-
41-
/**
42-
* Utility type for ensuring backwards compatibility on a type level:
43-
* - If there's a default slot, add 'children' to the props
44-
* - All props are bindable
45-
*/
46-
type PropsWithChildren<Props, Slots> = WithBindings<Props> &
26+
type Properties<Props, Slots> = Props &
4727
(Slots extends { default: any }
4828
? // This is unfortunate because it means "accepts no props" turns into "accepts any prop"
4929
// but the alternative is non-fixable type errors because of the way TypeScript index
@@ -95,13 +75,13 @@ export class SvelteComponent<
9575
* is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See
9676
* https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info.
9777
*/
98-
constructor(options: ComponentConstructorOptions<PropsWithChildren<Props, Slots>>);
78+
constructor(options: ComponentConstructorOptions<Properties<Props, Slots>>);
9979
/**
10080
* For type checking capabilities only.
10181
* Does not exist at runtime.
10282
* ### DO NOT USE!
10383
* */
104-
$$prop_def: RemoveBindable<Props>; // Without PropsWithChildren: unnecessary, causes type bugs
84+
$$prop_def: Props; // Without Properties: unnecessary, causes type bugs
10585
/**
10686
* For type checking capabilities only.
10787
* Does not exist at runtime.
@@ -116,6 +96,12 @@ export class SvelteComponent<
11696
*
11797
* */
11898
$$slot_def: Slots;
99+
/**
100+
* For type checking capabilities only.
101+
* Does not exist at runtime.
102+
* ### DO NOT USE!
103+
* */
104+
$$bindings?: string;
119105

120106
/**
121107
* @deprecated This method only exists when using one of the legacy compatibility helpers, which
@@ -181,7 +167,7 @@ export type ComponentEvents<Comp extends SvelteComponent> =
181167
* ```
182168
*/
183169
export type ComponentProps<Comp extends SvelteComponent> =
184-
Comp extends SvelteComponent<infer Props> ? RemoveBindable<Props> : never;
170+
Comp extends SvelteComponent<infer Props> ? Props : never;
185171

186172
/**
187173
* Convenience type to get the type of a Svelte component. Useful for example in combination with

packages/svelte/src/internal/client/render.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ export function stringify(value) {
9595
* @param {{
9696
* target: Document | Element | ShadowRoot;
9797
* anchor?: Node;
98-
* props?: import('../types.js').RemoveBindable<Props>;
98+
* props?: Props;
9999
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
100100
* context?: Map<any, any>;
101101
* intro?: boolean;
@@ -121,7 +121,7 @@ export function mount(component, options) {
121121
* @param {import('../../index.js').ComponentType<import('../../index.js').SvelteComponent<Props, Events>>} component
122122
* @param {{
123123
* target: Document | Element | ShadowRoot;
124-
* props?: import('../types.js').RemoveBindable<Props>;
124+
* props?: Props;
125125
* events?: { [Property in keyof Events]: (e: Events[Property]) => any };
126126
* context?: Map<any, any>;
127127
* intro?: boolean;

packages/svelte/src/internal/client/types.d.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import type { Bindable, Binding } from '../../index.js';
21
import type { Store } from '#shared';
32
import { STATE_SYMBOL } from './constants.js';
43
import type { Effect, Source, Value } from './reactivity/types.js';
Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,2 @@
1-
import type { Bindable } from '../index.js';
2-
31
/** Anything except a function */
42
export type NotFunction<T> = T extends Function ? never : T;
5-
6-
export type RemoveBindable<Props extends Record<string, any>> = {
7-
[Key in keyof Props]: Props[Key] extends Bindable<infer Value> ? Value : Props[Key];
8-
};

packages/svelte/tests/types/component.ts

Lines changed: 1 addition & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,7 @@ import {
55
type ComponentProps,
66
type ComponentType,
77
mount,
8-
hydrate,
9-
type Bindable,
10-
type Binding,
11-
type ComponentConstructorOptions
8+
hydrate
129
} from 'svelte';
1310

1411
SvelteComponent.element === HTMLElement;
@@ -177,53 +174,3 @@ const x: typeof asLegacyComponent = createClassComponent({
177174
hydrate: true,
178175
component: NewComponent
179176
});
180-
181-
// --------------------------------------------------------------------------- bindable
182-
183-
// Test that
184-
// - everything's bindable unless the component constructor is specifically set telling otherwise (for backwards compatibility)
185-
// - when using mount etc the props are never bindable because this is language-tools only concept
186-
187-
function binding<T>(value: T): Binding<T> {
188-
return value as any;
189-
}
190-
191-
class Explicit extends SvelteComponent<{
192-
foo: string;
193-
bar: Bindable<boolean>;
194-
}> {
195-
constructor(options: ComponentConstructorOptions<{ foo: string; bar: Bindable<boolean> }>) {
196-
super(options);
197-
}
198-
}
199-
new Explicit({ target: null as any, props: { foo: 'foo', bar: binding(true) } });
200-
new Explicit({ target: null as any, props: { foo: 'foo', bar: true } });
201-
new Explicit({
202-
target: null as any,
203-
props: {
204-
// @ts-expect-error
205-
foo: binding(''),
206-
bar: true
207-
}
208-
});
209-
mount(Explicit, { target: null as any, props: { foo: 'foo', bar: true } });
210-
mount(Explicit, {
211-
target: null as any,
212-
props: {
213-
// @ts-expect-error
214-
bar: binding(true)
215-
}
216-
});
217-
218-
class Implicit extends SvelteComponent<{ foo: string; bar: boolean }> {}
219-
new Implicit({ target: null as any, props: { foo: 'foo', bar: true } });
220-
new Implicit({ target: null as any, props: { foo: binding(''), bar: binding(true) } });
221-
mount(Implicit, { target: null as any, props: { foo: 'foo', bar: true } });
222-
mount(Implicit, {
223-
target: null as any,
224-
props: {
225-
foo: 'foo',
226-
// @ts-expect-error
227-
bar: binding(true)
228-
}
229-
});

packages/svelte/types/index.d.ts

Lines changed: 15 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -17,29 +17,10 @@ declare module 'svelte' {
1717
$$inline?: boolean;
1818
}
1919

20-
/** Tooling for types uses this for properties are being used with `bind:` */
21-
export type Binding<T> = { 'bind:': T };
2220
/**
23-
* Tooling for types uses this for properties that may be bound to.
24-
* Only use this if you author Svelte component type definition files by hand (we recommend using `@sveltejs/package` instead).
25-
* Example:
26-
* ```ts
27-
* export class MyComponent extends SvelteComponent<{ readonly: string, bindable: Bindable<string> }> {}
28-
* ```
29-
* means you can now do `<MyComponent {readonly} bind:bindable />`
30-
*/
31-
export type Bindable<T> = T | Binding<T>;
32-
33-
type WithBindings<T> = {
34-
[Key in keyof T]: Bindable<T[Key]>;
35-
};
36-
37-
/**
38-
* Utility type for ensuring backwards compatibility on a type level:
39-
* - If there's a default slot, add 'children' to the props
40-
* - All props are bindable
21+
* Utility type for ensuring backwards compatibility on a type level that if there's a default slot, add 'children' to the props
4122
*/
42-
type PropsWithChildren<Props, Slots> = WithBindings<Props> &
23+
type Properties<Props, Slots> = Props &
4324
(Slots extends { default: any }
4425
? // This is unfortunate because it means "accepts no props" turns into "accepts any prop"
4526
// but the alternative is non-fixable type errors because of the way TypeScript index
@@ -91,13 +72,13 @@ declare module 'svelte' {
9172
* is a stop-gap solution. Migrate towards using `mount` or `createRoot` instead. See
9273
* https://svelte-5-preview.vercel.app/docs/breaking-changes#components-are-no-longer-classes for more info.
9374
*/
94-
constructor(options: ComponentConstructorOptions<PropsWithChildren<Props, Slots>>);
75+
constructor(options: ComponentConstructorOptions<Properties<Props, Slots>>);
9576
/**
9677
* For type checking capabilities only.
9778
* Does not exist at runtime.
9879
* ### DO NOT USE!
9980
* */
100-
$$prop_def: RemoveBindable<Props>; // Without PropsWithChildren: unnecessary, causes type bugs
81+
$$prop_def: Props; // Without Properties: unnecessary, causes type bugs
10182
/**
10283
* For type checking capabilities only.
10384
* Does not exist at runtime.
@@ -112,6 +93,12 @@ declare module 'svelte' {
11293
*
11394
* */
11495
$$slot_def: Slots;
96+
/**
97+
* For type checking capabilities only.
98+
* Does not exist at runtime.
99+
* ### DO NOT USE!
100+
* */
101+
$$bindings?: string;
115102

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

182169
/**
183170
* Convenience type to get the type of a Svelte component. Useful for example in combination with
@@ -247,12 +234,6 @@ declare module 'svelte' {
247234
: [type: Type, parameter: EventMap[Type], options?: DispatchOptions]
248235
): boolean;
249236
}
250-
/** Anything except a function */
251-
type NotFunction<T> = T extends Function ? never : T;
252-
253-
type RemoveBindable<Props extends Record<string, any>> = {
254-
[Key in keyof Props]: Props[Key] extends Bindable<infer Value> ? Value : Props[Key];
255-
};
256237
/**
257238
* The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM.
258239
* It must be called during the component's initialisation (but doesn't need to live *inside* the component;
@@ -323,14 +304,16 @@ declare module 'svelte' {
323304
* Synchronously flushes any pending state changes and those that result from it.
324305
* */
325306
export function flushSync(fn?: (() => void) | undefined): void;
307+
/** Anything except a function */
308+
type NotFunction<T> = T extends Function ? never : T;
326309
/**
327310
* Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component
328311
*
329312
* */
330313
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: {
331314
target: Document | Element | ShadowRoot;
332315
anchor?: Node | undefined;
333-
props?: RemoveBindable<Props> | undefined;
316+
props?: Props | undefined;
334317
events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined;
335318
context?: Map<any, any> | undefined;
336319
intro?: boolean | undefined;
@@ -341,7 +324,7 @@ declare module 'svelte' {
341324
* */
342325
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: {
343326
target: Document | Element | ShadowRoot;
344-
props?: RemoveBindable<Props> | undefined;
327+
props?: Props | undefined;
345328
events?: { [Property in keyof Events]: (e: Events[Property]) => any; } | undefined;
346329
context?: Map<any, any> | undefined;
347330
intro?: boolean | undefined;

0 commit comments

Comments
 (0)