Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tangy-tips-exist.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'svelte': patch
---

fix too complex type error when using union props type by
1 change: 0 additions & 1 deletion packages/svelte/src/compiler/types/index.d.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
6 changes: 5 additions & 1 deletion packages/svelte/src/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ type Branded<T, B> = T & Brand<B>;
*/
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;

/**
* Can be used to create strongly typed Svelte components.
*
Expand All @@ -141,7 +145,7 @@ export type ComponentInternals = Branded<{}, 'ComponentInternals'>;
export interface Component<
Props extends Record<string, any> = {},
Exports extends Record<string, any> = {},
Bindings extends keyof Props | '' = string
Bindings extends keyof UnionToIntersection<Required<Props>> | (string & {}) | '' = string
> {
/**
* @param internal An internal object used by Svelte. Do not use or modify.
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/server/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>} Props
* @param {Component<Props> | ComponentType<SvelteComponent<Props>>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
* @param {{ props?: NoInfer<Props>; context?: Map<any, any>; idPrefix?: string }} [options]
* @returns {RenderOutput}
*/
export function render(component, options = {}) {
Expand Down
8 changes: 4 additions & 4 deletions packages/svelte/src/internal/server/renderer.js
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, any>} Props
* @param {Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} [options]
* @param {{ props?: NoInfer<Props>; context?: Map<any, any>; idPrefix?: string }} [options]
* @returns {RenderOutput}
*/
static render(component, options = {}) {
Expand Down Expand Up @@ -493,7 +493,7 @@ export class Renderer {
*
* @template {Record<string, any>} Props
* @param {Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
* @param {{ props?: NoInfer<Props>; context?: Map<any, any>; idPrefix?: string }} options
* @returns {AccumulatedContent}
*/
static #render(component, options) {
Expand All @@ -514,7 +514,7 @@ export class Renderer {
*
* @template {Record<string, any>} Props
* @param {Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
* @param {{ props?: NoInfer<Props>; context?: Map<any, any>; idPrefix?: string }} options
* @returns {Promise<AccumulatedContent>}
*/
static async #render_async(component, options) {
Expand Down Expand Up @@ -592,7 +592,7 @@ export class Renderer {
* @template {Record<string, any>} Props
* @param {'sync' | 'async'} mode
* @param {import('svelte').Component<Props>} component
* @param {{ props?: Omit<Props, '$$slots' | '$$events'>; context?: Map<any, any>; idPrefix?: string }} options
* @param {{ props?: NoInfer<Props>; context?: Map<any, any>; idPrefix?: string }} options
* @returns {Renderer}
*/
static #open_render(mode, component, options) {
Expand Down
4 changes: 2 additions & 2 deletions packages/svelte/src/server/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ export function render<
? [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options?: {
props?: Omit<Props, '$$slots' | '$$events'>;
props?: NoInfer<Props>;
context?: Map<any, any>;
idPrefix?: string;
}
]
: [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options: {
props: Omit<Props, '$$slots' | '$$events'>;
props: NoInfer<Props>;
context?: Map<any, any>;
idPrefix?: string;
}
Expand Down
149 changes: 149 additions & 0 deletions packages/svelte/tests/types/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
type ComponentInternals
} from 'svelte';
import { render } from 'svelte/server';
import type { HTMLAnchorAttributes, HTMLButtonAttributes } from '../../elements';

SvelteComponent.element === HTMLElement;

Expand Down Expand Up @@ -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<any> = 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<typeof unionComponent> = {
binding: true,
readonly: 'foo',
type: 'button',
// @ts-expect-error
prop: 1
};

const unionComponentProps2: ComponentProps<typeof unionComponent> = {
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
Expand Down
34 changes: 19 additions & 15 deletions packages/svelte/types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,10 @@ declare module 'svelte' {
*/
export type ComponentInternals = Branded<{}, 'ComponentInternals'>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void
? I
: never;

/**
* Can be used to create strongly typed Svelte components.
*
Expand All @@ -140,7 +144,7 @@ declare module 'svelte' {
export interface Component<
Props extends Record<string, any> = {},
Exports extends Record<string, any> = {},
Bindings extends keyof Props | '' = string
Bindings extends keyof UnionToIntersection<Required<Props>> | (string & {}) | '' = string
> {
/**
* @param internal An internal object used by Svelte. Do not use or modify.
Expand Down Expand Up @@ -2177,10 +2181,10 @@ declare module 'svelte/motion' {
* const tween = Tween.of(() => number);
* </script>
* ```
*
*
*/
static of<U>(fn: () => U, options?: TweenedOptions<U> | undefined): Tween<U>;

constructor(value: T, options?: TweenedOptions<T>);
/**
* Sets `tween.target` to `value` and returns a `Promise` that resolves if and when `tween.current` catches up to it.
Expand Down Expand Up @@ -2231,7 +2235,7 @@ declare module 'svelte/reactivity' {
* ```
*/
export class SvelteDate extends Date {

constructor(...params: any[]);
#private;
}
Expand Down Expand Up @@ -2267,12 +2271,12 @@ declare module 'svelte/reactivity' {
* {#if monkeys.has('🙊')}<p>speak no evil</p>{/if}
* ```
*
*
*
*/
export class SvelteSet<T> extends Set<T> {

constructor(value?: Iterable<T> | null | undefined);

add(value: T): this;
#private;
}
Expand Down Expand Up @@ -2318,12 +2322,12 @@ declare module 'svelte/reactivity' {
* {/if}
* ```
*
*
*
*/
export class SvelteMap<K, V> extends Map<K, V> {

constructor(value?: Iterable<readonly [K, V]> | null | undefined);

set(key: K, value: V): this;
#private;
}
Expand Down Expand Up @@ -2386,7 +2390,7 @@ declare module 'svelte/reactivity' {
* ```
*/
export class SvelteURLSearchParams extends URLSearchParams {

[REPLACE](params: URLSearchParams): void;
#private;
}
Expand Down Expand Up @@ -2462,7 +2466,7 @@ declare module 'svelte/reactivity' {
*/
export function createSubscriber(start: (update: () => void) => (() => void) | void): () => void;
class ReactiveValue<T> {

constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
Expand Down Expand Up @@ -2527,7 +2531,7 @@ declare module 'svelte/reactivity/window' {
get current(): number | undefined;
};
class ReactiveValue<T> {

constructor(fn: () => T, onsubscribe: (update: () => void) => void);
get current(): T;
#private;
Expand All @@ -2550,15 +2554,15 @@ declare module 'svelte/server' {
? [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options?: {
props?: Omit<Props, '$$slots' | '$$events'>;
props?: NoInfer<Props>;
context?: Map<any, any>;
idPrefix?: string;
}
]
: [
component: Comp extends SvelteComponent<any> ? ComponentType<Comp> : Comp,
options: {
props: Omit<Props, '$$slots' | '$$events'>;
props: NoInfer<Props>;
context?: Map<any, any>;
idPrefix?: string;
}
Expand Down