Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: ensure createEventDispatcher works with types from generics #8872

Merged
merged 2 commits into from
Jun 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

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

fix: ensure `createEventDispatcher` and `ActionReturn` work with types from generic function parameters
2 changes: 1 addition & 1 deletion documentation/docs/05-misc/03-typescript.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ Events can be typed with `createEventDispatcher`:
import { createEventDispatcher } from 'svelte';

const dispatch = createEventDispatcher<{
event: never; // does not accept a payload
event: null; // does not accept a payload
type: string; // has a required string payload
click: string | null; // has an optional string payload
}>();
Expand Down
6 changes: 3 additions & 3 deletions documentation/docs/05-misc/04-v4-migration-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher<{
optional: number | null;
required: string;
noArgument: never;
noArgument: null;
}>();

// Svelte version 3:
Expand All @@ -50,10 +50,10 @@ dispatch('required'); // error, missing argument
dispatch('noArgument', 'surprise'); // error, cannot pass an argument
```

- `Action` and `ActionReturn` have a default parameter type of `never` now, which means you need to type the generic if you want to specify that this action receives a parameter. The migration script will migrate this automatically ([#7442](https://github.com/sveltejs/svelte/pull/7442))
- `Action` and `ActionReturn` have a default parameter type of `undefined` now, which means you need to type the generic if you want to specify that this action receives a parameter. The migration script will migrate this automatically ([#7442](https://github.com/sveltejs/svelte/pull/7442))

```diff
-const action: Action = (node, params) => { .. } // this is now an error, as params is expected to not exist
-const action: Action = (node, params) => { .. } // this is now an error if you use params in any way
+const action: Action<HTMLElement, string> = (node, params) => { .. } // params is of type string
```

Expand Down
17 changes: 7 additions & 10 deletions packages/svelte/src/runtime/action/public.d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/**
* Actions can return an object containing the two properties defined in this interface. Both are optional.
* - update: An action can have a parameter. This method will be called whenever that parameter changes,
* immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn<never>` both
* mean that the action accepts no parameters, which makes it illegal to set the `update` method.
* immediately after Svelte has applied updates to the markup. `ActionReturn` and `ActionReturn<undefined>` both
* mean that the action accepts no parameters.
* - destroy: Method that is called after the element is unmounted
*
* Additionally, you can specify which additional attributes and events the action enables on the applied element.
Expand All @@ -27,10 +27,10 @@
* Docs: https://svelte.dev/docs/svelte-action
*/
export interface ActionReturn<
Parameter = never,
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
update?: [Parameter] extends [never] ? never : (parameter: Parameter) => void;
update?: (parameter: Parameter) => void;
destroy?: () => void;
/**
* ### DO NOT USE THIS
Expand All @@ -50,7 +50,7 @@ export interface ActionReturn<
* // ...
* }
* ```
* `Action<HTMLDivElement>` and `Action<HTMLDiveElement, never>` both signal that the action accepts no parameters.
* `Action<HTMLDivElement>` and `Action<HTMLDiveElement, undefined>` both signal that the action accepts no parameters.
*
* You can return an object with methods `update` and `destroy` from the function and type which additional attributes and events it has.
* See interface `ActionReturn` for more details.
Expand All @@ -59,18 +59,15 @@ export interface ActionReturn<
*/
export interface Action<
Element = HTMLElement,
Parameter = never,
Parameter = undefined,
Attributes extends Record<string, any> = Record<never, any>
> {
<Node extends Element>(
...args: [Parameter] extends [never]
? [node: Node]
: undefined extends Parameter
...args: undefined extends Parameter
? [node: Node, parameter?: Parameter]
: [node: Node, parameter: Parameter]
): void | ActionReturn<Parameter, Attributes>;
}

// Implementation notes:
// - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode
// - [X] extends [never] is needed, X extends never would reduce the whole resulting type to never and not to one of the condition outcomes
10 changes: 4 additions & 6 deletions packages/svelte/src/runtime/internal/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,12 @@ export interface DispatchOptions {
export interface EventDispatcher<EventMap extends Record<string, any>> {
// Implementation notes:
// - undefined extends X instead of X extends undefined makes this work better with both strict and nonstrict mode
// - [X] extends [never] is needed, X extends never would reduce the whole resulting type to never and not to one of the condition outcomes
// - | null | undefined is added for convenience, as they are equivalent for the custom event constructor (both result in a null detail)
<Type extends keyof EventMap>(
...args: [EventMap[Type]] extends [never]
? [type: Type, parameter?: null | undefined, options?: DispatchOptions]
: null extends EventMap[Type]
? [type: Type, parameter?: EventMap[Type], options?: DispatchOptions]
...args: null extends EventMap[Type]
? [type: Type, parameter?: EventMap[Type] | null | undefined, options?: DispatchOptions]
: undefined extends EventMap[Type]
? [type: Type, parameter?: EventMap[Type], options?: DispatchOptions]
? [type: Type, parameter?: EventMap[Type] | null | undefined, options?: DispatchOptions]
: [type: Type, parameter: EventMap[Type], options?: DispatchOptions]
): boolean;
}
21 changes: 9 additions & 12 deletions packages/svelte/test/types/actions.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { Action, ActionReturn } from '$runtime/action';
import type { Action, ActionReturn } from '$runtime/action/public';

// ---------------- Action

Expand Down Expand Up @@ -65,30 +65,27 @@ const optional4: Action<HTMLElement, boolean | undefined> = (_node, _param?) =>
};
optional4;

const no: Action<HTMLElement, never> = (_node) => {};
const no: Action<HTMLElement, undefined> = (_node) => {};
// @ts-expect-error second param
no(null as any, true);
no(null as any);
// @ts-expect-error second param
no(null as any, 'string');

const no1: Action<HTMLElement, never> = (_node) => {
const no1: Action<HTMLElement, undefined> = (_node) => {
return {
destroy: () => {}
};
};
no1;

// @ts-expect-error param given
const no2: Action<HTMLElement, never> = (_node, _param?) => {};
no2;
const no2: Action<HTMLElement, undefined> = (_node, _param?) => {};
no2(null as any);

// @ts-expect-error param given
const no3: Action<HTMLElement, never> = (_node, _param) => {};
const no3: Action<HTMLElement, undefined> = (_node, _param) => {};
no3;

// @ts-expect-error update method given
const no4: Action<HTMLElement, never> = (_node) => {
const no4: Action<HTMLElement, undefined> = (_node) => {
return {
update: () => {},
destroy: () => {}
Expand All @@ -106,7 +103,7 @@ requiredReturn;
const optionalReturn: ActionReturn<boolean | undefined> = {
update: (p) => {
p === true;
// @ts-expect-error could be undefined
// @ts-expect-error (only in strict mode) could be undefined
p.toString();
}
};
Expand All @@ -118,7 +115,7 @@ const invalidProperty: ActionReturn = {
};
invalidProperty;

type Attributes = ActionReturn<never, { a: string }>['$$_attributes'];
type Attributes = ActionReturn<undefined, { a: string }>['$$_attributes'];
const attributes: Attributes = { a: 'a' };
attributes;
// @ts-expect-error wrong type
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/test/types/create-event-dispatcher.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createEventDispatcher } from '$runtime/internal/lifecycle';

const dispatch = createEventDispatcher<{
loaded: never;
loaded: null;
change: string;
valid: boolean;
optional: number | null;
Expand Down