Skip to content

Commit 8e51b51

Browse files
ivanhoferdummdidumm
andcommitted
breaking: improve types for createEventDispatcher (#7224)
--------- Co-authored-by: Simon Holthausen <[email protected]>
1 parent caef440 commit 8e51b51

File tree

6 files changed

+95
-14
lines changed

6 files changed

+95
-14
lines changed

.github/workflows/ci.yml

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,17 @@ jobs:
4646
timeout-minutes: 10
4747
strategy:
4848
matrix:
49-
node-version: 14
50-
os: [ubuntu-latest, windows-latest, macOS-latest]
49+
include:
50+
- node-version: 14
51+
os: ubuntu-latest
52+
- node-version: 14
53+
os: windows-latest
54+
- node-version: 14
55+
os: macOS-latest
56+
- node-version: 16
57+
os: ubuntu-latest
58+
- node-version: 18
59+
os: ubuntu-latest
5160
steps:
5261
- uses: actions/checkout@v3
5362
- uses: actions/setup-node@v3

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
## Unreleased (4.0)
44

5-
* Minimum supported Node version is now Node 14
5+
* **breaking** Minimum supported Node version is now Node 14
6+
* **breaking** Minimum supported TypeScript version is now 5 (it will likely work with lower versions, but we make no guarantess about that)
7+
* **breaking** Stricter types for `createEventDispatcher` (see PR for migration instructions) ([#7224](https://github.com/sveltejs/svelte/pull/7224))
68

79
## Unreleased (3.0)
810

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
},
9191
"types": "types/runtime/index.d.ts",
9292
"scripts": {
93-
"test": "npm run test:unit && npm run test:integration",
93+
"test": "npm run test:unit && npm run test:integration && echo \"manually check that there are no type errors in test/types by opening the files in there\"",
9494
"test:integration": "mocha --exit",
9595
"test:unit": "mocha --config .mocharc.unit.js --exit",
9696
"quicktest": "mocha --exit",

src/runtime/internal/lifecycle.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,14 @@ export function onDestroy(fn: () => any) {
5656
get_current_component().$$.on_destroy.push(fn);
5757
}
5858

59+
export interface EventDispatcher<EventMap extends Record<string, any>> {
60+
<Type extends keyof EventMap>(
61+
...args: [EventMap[Type]] extends [never] ? [type: Type, parameter?: null | undefined, options?: DispatchOptions] :
62+
null extends EventMap[Type] ? [type: Type, parameter?: EventMap[Type], options?: DispatchOptions] :
63+
undefined extends EventMap[Type] ? [type: Type, parameter?: EventMap[Type], options?: DispatchOptions] :
64+
[type: Type, parameter: EventMap[Type], options?: DispatchOptions]): boolean;
65+
}
66+
5967
export interface DispatchOptions {
6068
cancelable?: boolean;
6169
}
@@ -68,20 +76,23 @@ export interface DispatchOptions {
6876
* [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent).
6977
* These events do not [bubble](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture).
7078
* The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail)
71-
* property and can contain any type of data.
79+
* property and can contain any type of data.
80+
*
81+
* The event dispatcher can be typed to narrow the allowed event names and the type of the `detail` argument:
82+
* ```ts
83+
* const dispatch = createEventDispatcher<{
84+
* loaded: never; // does not take a detail argument
85+
* change: string; // takes a detail argument of type string, which is required
86+
* optional: number | null; // takes an optional detail argument of type number
87+
* }>();
88+
* ```
7289
*
7390
* https://svelte.dev/docs#run-time-svelte-createeventdispatcher
7491
*/
75-
export function createEventDispatcher<EventMap extends {} = any>(): <
76-
EventKey extends Extract<keyof EventMap, string>
77-
>(
78-
type: EventKey,
79-
detail?: EventMap[EventKey],
80-
options?: DispatchOptions
81-
) => boolean {
92+
export function createEventDispatcher<EventMap extends Record<string, any> = any>(): EventDispatcher<EventMap> {
8293
const component = get_current_component();
8394

84-
return (type: string, detail?: any, { cancelable = false } = {}): boolean => {
95+
return ((type: string, detail?: any, { cancelable = false } = {}): boolean => {
8596
const callbacks = component.$$.callbacks[type];
8697

8798
if (callbacks) {
@@ -95,7 +106,7 @@ export function createEventDispatcher<EventMap extends {} = any>(): <
95106
}
96107

97108
return true;
98-
};
109+
}) as EventDispatcher<EventMap>;
99110
}
100111

101112
/**

test/types/create-event-dispatcher.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { createEventDispatcher } from '$runtime/internal/lifecycle';
2+
3+
const dispatch = createEventDispatcher<{
4+
loaded: never
5+
change: string
6+
valid: boolean
7+
optional: number | null
8+
}>();
9+
10+
// @ts-expect-error: dispatch invalid event
11+
dispatch('some-event');
12+
13+
dispatch('loaded');
14+
dispatch('loaded', null);
15+
dispatch('loaded', undefined);
16+
dispatch('loaded', undefined, { cancelable: true });
17+
// @ts-expect-error: no detail accepted
18+
dispatch('loaded', 123);
19+
20+
// @ts-expect-error: detail not provided
21+
dispatch('change');
22+
dispatch('change', 'string');
23+
dispatch('change', 'string', { cancelable: true });
24+
// @ts-expect-error: wrong type of detail
25+
dispatch('change', 123);
26+
// @ts-expect-error: wrong type of detail
27+
dispatch('change', undefined);
28+
29+
dispatch('valid', true);
30+
dispatch('valid', true, { cancelable: true });
31+
// @ts-expect-error: wrong type of detail
32+
dispatch('valid', 'string');
33+
34+
dispatch('optional');
35+
dispatch('optional', 123);
36+
dispatch('optional', 123, { cancelable: true });
37+
dispatch('optional', null);
38+
dispatch('optional', undefined);
39+
dispatch('optional', undefined, { cancelable: true });
40+
// @ts-expect-error: wrong type of optional detail
41+
dispatch('optional', 'string');
42+
// @ts-expect-error: wrong type of option
43+
dispatch('optional', undefined, { cancelabled: true });

test/types/tsconfig.json

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"extends": "../../tsconfig.json",
3+
"compilerOptions": {
4+
"rootDir": "../..",
5+
"baseUrl": "../../",
6+
"paths": {
7+
"$runtime/*": ["src/runtime/*"]
8+
},
9+
// enable strictest options
10+
"allowUnreachableCode": false,
11+
"noFallthroughCasesInSwitch": true,
12+
"noImplicitReturns": true,
13+
"strict": true,
14+
},
15+
"include": ["."]
16+
}

0 commit comments

Comments
 (0)