diff --git a/.changeset/tangy-tips-exist.md b/.changeset/tangy-tips-exist.md new file mode 100644 index 0000000000..ccd049acba --- /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 c04466a24d..7385aa4374 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 7823c8e33e..30abddd5f5 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -117,6 +117,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. * @@ -142,7 +146,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 12f76f188e..1977351e41 100644 --- a/packages/svelte/src/internal/server/index.js +++ b/packages/svelte/src/internal/server/index.js @@ -65,7 +65,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; csp?: Csp; transformError?: (error: unknown) => unknown }} [options] + * @param {{ props?: Props; context?: Map; idPrefix?: string; csp?: Csp; transformError?: (error: unknown) => unknown }} [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 35aac64721..7359d895b8 100644 --- a/packages/svelte/src/internal/server/renderer.js +++ b/packages/svelte/src/internal/server/renderer.js @@ -506,7 +506,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; csp?: Csp }} [options] + * @param {{ props?: Props; context?: Map; idPrefix?: string; csp?: Csp }} [options] * @returns {RenderOutput} */ static render(component, options = {}) { @@ -629,7 +629,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) { @@ -650,7 +650,7 @@ export class Renderer { * * @template {Record} Props * @param {Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string; csp?: Csp }} options + * @param {{ props?: Props; context?: Map; idPrefix?: string; csp?: Csp }} options * @returns {Promise} */ static async #render_async(component, options) { @@ -760,7 +760,7 @@ export class Renderer { * @template {Record} Props * @param {'sync' | 'async'} mode * @param {import('svelte').Component} component - * @param {{ props?: Omit; context?: Map; idPrefix?: string; csp?: Csp; transformError?: (error: unknown) => unknown }} options + * @param {{ props?: Props; context?: Map; idPrefix?: string; csp?: Csp; transformError?: (error: unknown) => unknown }} 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 db75afb9ed..7404f5b860 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?: Props; context?: Map; idPrefix?: string; csp?: Csp; @@ -23,7 +23,7 @@ export function render< : [ component: Comp extends SvelteComponent ? ComponentType : Comp, options: { - props: Omit; + props: Props; context?: Map; idPrefix?: string; csp?: Csp; diff --git a/packages/svelte/tests/types/component.ts b/packages/svelte/tests/types/component.ts index 06749e9937..95611710f9 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 5f41fabf60..d0bf0fee3a 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -116,6 +116,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. * @@ -141,7 +145,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. @@ -2210,7 +2214,7 @@ declare module 'svelte/motion' { * const tween = Tween.of(() => number); * * ``` - * + * */ static of(fn: () => U, options?: TweenOptions | undefined): Tween; @@ -2264,7 +2268,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteDate extends Date { - + constructor(...params: any[]); #private; } @@ -2300,12 +2304,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; } @@ -2351,12 +2355,12 @@ declare module 'svelte/reactivity' { * {/if} * ``` * - * + * */ export class SvelteMap extends Map { - + constructor(value?: Iterable | null | undefined); - + set(key: K, value: V): this; #private; } @@ -2419,7 +2423,7 @@ declare module 'svelte/reactivity' { * ``` */ export class SvelteURLSearchParams extends URLSearchParams { - + [REPLACE](params: URLSearchParams): void; #private; } @@ -2495,7 +2499,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; @@ -2560,7 +2564,7 @@ declare module 'svelte/reactivity/window' { get current(): number | undefined; }; class ReactiveValue { - + constructor(fn: () => T, onsubscribe: (update: () => void) => void); get current(): T; #private; @@ -2583,7 +2587,7 @@ declare module 'svelte/server' { ? [ component: Comp extends SvelteComponent ? ComponentType : Comp, options?: { - props?: Omit; + props?: Props; context?: Map; idPrefix?: string; csp?: Csp; @@ -2593,7 +2597,7 @@ declare module 'svelte/server' { : [ component: Comp extends SvelteComponent ? ComponentType : Comp, options: { - props: Omit; + props: Props; context?: Map; idPrefix?: string; csp?: Csp;