diff --git a/.changeset/tangy-tips-exist.md b/.changeset/tangy-tips-exist.md new file mode 100644 index 000000000000..ccd049acbafa --- /dev/null +++ b/.changeset/tangy-tips-exist.md @@ -0,0 +1,5 @@ +--- +'svelte': patch +--- + +fix too complex type error when using union props type by diff --git a/packages/svelte/src/compiler/types/index.d.ts b/packages/svelte/src/compiler/types/index.d.ts index fce3f62c5cff..c6f817022a27 100644 --- a/packages/svelte/src/compiler/types/index.d.ts +++ b/packages/svelte/src/compiler/types/index.d.ts @@ -1,5 +1,4 @@ import type { SourceMap } from 'magic-string'; -import type { Binding } from '../phases/scope.js'; import type { AST, Namespace } from './template.js'; import type { ICompileDiagnostic } from '../utils/compile_diagnostic.js'; import type { StateCreationRuneName } from '../../utils.js'; diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index a1782f5b61a5..70c41c731b00 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -116,6 +116,10 @@ type Branded = T & Brand; */ export type ComponentInternals = Branded<{}, 'ComponentInternals'>; +type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void + ? I + : never; + /** * Can be used to create strongly typed Svelte components. * @@ -141,7 +145,7 @@ export type ComponentInternals = Branded<{}, 'ComponentInternals'>; export interface Component< Props extends Record = {}, Exports extends Record = {}, - Bindings extends keyof Props | '' = string + Bindings extends keyof UnionToIntersection> | (string & {}) | '' = string > { /** * @param internal An internal object used by Svelte. Do not use or modify. diff --git a/packages/svelte/src/internal/server/index.js b/packages/svelte/src/internal/server/index.js index c0dbdbda14f6..1ac1bd502dc3 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -56,7 +56,7 @@ export function element(renderer, tag, attributes_fn = noop, children_fn = noop) * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {Component | ComponentType>} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] + * @param {{ props?: NoInfer; context?: Map; idPrefix?: string }} [options] * @returns {RenderOutput} */ export function render(component, options = {}) { diff --git a/packages/svelte/src/internal/server/renderer.js b/packages/svelte/src/internal/server/renderer.js index 0cfb1a7a9351..582bdb4b3999 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -376,7 +376,7 @@ export class Renderer { * Takes a component and returns an object with `body` and `head` properties on it, which you can use to populate the HTML when server-rendering your app. * @template {Record} Props * @param {Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} [options] + * @param {{ props?: NoInfer; context?: Map; idPrefix?: string }} [options] * @returns {RenderOutput} */ static render(component, options = {}) { @@ -493,7 +493,7 @@ export class Renderer { * * @template {Record} Props * @param {Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options + * @param {{ props?: NoInfer; context?: Map; idPrefix?: string }} options * @returns {AccumulatedContent} */ static #render(component, options) { @@ -514,7 +514,7 @@ export class Renderer { * * @template {Record} Props * @param {Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options + * @param {{ props?: NoInfer; context?: Map; idPrefix?: string }} options * @returns {Promise} */ static async #render_async(component, options) { @@ -592,7 +592,7 @@ export class Renderer { * @template {Record} Props * @param {'sync' | 'async'} mode * @param {import('svelte').Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string }} options + * @param {{ props?: NoInfer; context?: Map; idPrefix?: string }} options * @returns {Renderer} */ static #open_render(mode, component, options) { diff --git a/packages/svelte/src/server/index.d.ts b/packages/svelte/src/server/index.d.ts index d5a3b813e6cb..3291d44fa82b 100644 --- a/packages/svelte/src/server/index.d.ts +++ b/packages/svelte/src/server/index.d.ts @@ -13,7 +13,7 @@ export function render< ? [ component: Comp extends SvelteComponent ? ComponentType : Comp, options?: { - props?: Omit; + props?: NoInfer; context?: Map; idPrefix?: string; } @@ -21,7 +21,7 @@ export function render< : [ component: Comp extends SvelteComponent ? ComponentType : Comp, options: { - props: Omit; + props: NoInfer; context?: Map; idPrefix?: string; } diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 06749e993776..95611710f900 100644 --- a/packages/svelte/tests/types/component.ts +++ b/packages/svelte/tests/types/component.ts @@ -10,6 +10,7 @@ import { type ComponentInternals } from 'svelte'; import { render } from 'svelte/server'; +import type { HTMLAnchorAttributes, HTMLButtonAttributes } from '../../elements'; SvelteComponent.element === HTMLElement; @@ -337,6 +338,154 @@ render(functionComponent, { } }); +// --------------------------------------------------------------------------- union props + +const unionComponent: Component< + { + binding: boolean; + } & ( + | (HTMLButtonAttributes & { + readonly: string; + }) + | (HTMLAnchorAttributes & { + readonly2: string; + }) + ), + { foo: 'bar' }, + 'binding' +> = (a, props) => { + props.binding === true; + if ('readonly' in props) { + props.readonly === 'foo'; + // @ts-expect-error + props.readonly = true; + } + if ('readonly2' in props) { + props.readonly2 === 'foo'; + // @ts-expect-error + props.readonly2 = true; + } + // @ts-expect-error + props.binding = ''; + return { + foo: 'bar' + }; +}; +unionComponent.element === HTMLElement; + +const unionBindingIsOkayToWiden: Component = unionComponent; + +unionComponent(null as any, { + binding: true, + // @ts-expect-error + readonly: true +}); + +const unionComponentInstance = unionComponent(null as any, { + binding: true, + readonly: 'foo', + // @ts-expect-error + x: '' +}); +unionComponentInstance.foo === 'bar'; +// @ts-expect-error +unionComponentInstance.foo = 'foo'; + +const unionComponentProps: ComponentProps = { + binding: true, + readonly: 'foo', + type: 'button', + // @ts-expect-error + prop: 1 +}; + +const unionComponentProps2: ComponentProps = { + binding: true, + readonly2: 'foo', + href: 'https://example.com', + // @ts-expect-error + prop: 1 +}; + +// Test that self-typed functions are correctly inferred, too (use case: language tools has its own shape for backwards compatibility) +const unionComponentProps3: ComponentProps<(a: any, b: { a: true }) => { foo: string }> = { + a: true +}; + +mount(unionComponent, { + target: null as any as Document | Element | ShadowRoot, + props: { + binding: true, + readonly: 'foo', + type: 'button', + // @ts-expect-error + x: '' + } +}); +mount(unionComponent, { + target: null as any as Document | Element | ShadowRoot, + props: { + binding: true, + // @ts-expect-error wrong type + readonly: 1 + } +}); +mount( + unionComponent, + // @ts-expect-error props missing + { target: null as any } +); +// if component receives no args, props can be omitted +mount(null as any as Component<{}>, { target: null as any }); + +hydrate(unionComponent, { + target: null as any as Document | Element | ShadowRoot, + props: { + binding: true, + readonly: 'foo', + type: 'button', + // @ts-expect-error + x: '' + } +}); +hydrate(unionComponent, { + target: null as any as Document | Element | ShadowRoot, + // @ts-expect-error missing prop + props: { + binding: true + } +}); +hydrate( + unionComponent, + // @ts-expect-error props missing + { target: null as any } +); +// if component receives no args, props can be omitted +hydrate(null as any as Component<{}>, { target: null as any }); + +render(unionComponent, { + props: { + binding: true, + readonly: 'foo', + type: 'button' + } +}); +// @ts-expect-error +render(unionComponent); +render(unionComponent, { + // @ts-expect-error + props: { + binding: true + } +}); +render(unionComponent, { + // @ts-expect-error + props: { + binding: true, + readonly: 1 + } +}); + // --------------------------------------------------------------------------- *.svelte components // import from a nonexistent file to trigger the declare module '*.svelte' in ambient.d.ts diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 9ace341e1609..53b9a3d1aec4 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -115,6 +115,10 @@ declare module 'svelte' { */ export type ComponentInternals = Branded<{}, 'ComponentInternals'>; + type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void + ? I + : never; + /** * Can be used to create strongly typed Svelte components. * @@ -140,7 +144,7 @@ declare module 'svelte' { export interface Component< Props extends Record = {}, Exports extends Record = {}, - Bindings extends keyof Props | '' = string + Bindings extends keyof UnionToIntersection> | (string & {}) | '' = string > { /** * @param internal An internal object used by Svelte. Do not use or modify. @@ -2177,10 +2181,10 @@ declare module 'svelte/motion' { * const tween = Tween.of(() => number); * * ``` - * + * */ static of(fn: () => U, options?: TweenedOptions | undefined): Tween; - + constructor(value: T, options?: TweenedOptions); /** * Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it. @@ -2231,7 +2235,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteDate extends Date { - + constructor(...params: any[]); #private; } @@ -2267,12 +2271,12 @@ declare module 'svelte/reactivity' { * {#if monkeys.has('🙊')}

speak no evil

{/if} * ``` * - * + * */ export class SvelteSet extends Set { - + constructor(value?: Iterable | null | undefined); - + add(value: T): this; #private; } @@ -2318,12 +2322,12 @@ declare module 'svelte/reactivity' { * {/if} * ``` * - * + * */ export class SvelteMap extends Map { - + constructor(value?: Iterable | null | undefined); - + set(key: K, value: V): this; #private; } @@ -2386,7 +2390,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteURLSearchParams extends URLSearchParams { - + [REPLACE](params: URLSearchParams): void; #private; } @@ -2462,7 +2466,7 @@ declare module 'svelte/reactivity' { */ export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void; class ReactiveValue { - + constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; @@ -2527,7 +2531,7 @@ declare module 'svelte/reactivity/window' { get current(): number | undefined; }; class ReactiveValue { - + constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; @@ -2550,7 +2554,7 @@ declare module 'svelte/server' { ? [ component: Comp extends SvelteComponent ? ComponentType : Comp, options?: { - props?: Omit; + props?: NoInfer; context?: Map; idPrefix?: string; } @@ -2558,7 +2562,7 @@ declare module 'svelte/server' { : [ component: Comp extends SvelteComponent ? ComponentType : Comp, options: { - props: Omit; + props: NoInfer; context?: Map; idPrefix?: string; }