Skip to content

Commit

Permalink
fix: ensure createEventDispatcher and ActionReturn work with generic …
Browse files Browse the repository at this point in the history
…function types (#8872)

fixes #8860

This contains a small but unfortunately unavoidable breaking change: If you used `never` to type that the second parameter of `createEventDispatcher` shouldn't be set or that the action accepts no parameters (which the docs recommended for a short time), then you need to change that to `null` and `undefined` respectively
  • Loading branch information
dummdidumm committed Jun 28, 2023
1 parent 7934a7f commit b0a3fa1
Show file tree
Hide file tree
Showing 7 changed files with 30 additions and 33 deletions.
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

0 comments on commit b0a3fa1

Please sign in to comment.