Skip to content

Commit

Permalink
breaking: improve types for createEventDispatcher (#7224)
Browse files Browse the repository at this point in the history
---------

Co-authored-by: Simon Holthausen <simon.holthausen@vercel.com>
  • Loading branch information
ivanhofer and dummdidumm committed Apr 14, 2023
1 parent 573784c commit d6bcddd
Show file tree
Hide file tree
Showing 6 changed files with 95 additions and 14 deletions.
13 changes: 11 additions & 2 deletions .github/workflows/ci.yml
Expand Up @@ -46,8 +46,17 @@ jobs:
timeout-minutes: 10
strategy:
matrix:
node-version: 14
os: [ubuntu-latest, windows-latest, macOS-latest]
include:
- node-version: 14
os: ubuntu-latest
- node-version: 14
os: windows-latest
- node-version: 14
os: macOS-latest
- node-version: 16
os: ubuntu-latest
- node-version: 18
os: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
Expand Down
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -2,7 +2,9 @@

## Unreleased (4.0)

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

## Unreleased (3.0)

Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -90,7 +90,7 @@
},
"types": "types/runtime/index.d.ts",
"scripts": {
"test": "npm run test:unit && npm run test:integration",
"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\"",
"test:integration": "mocha --exit",
"test:unit": "mocha --config .mocharc.unit.js --exit",
"quicktest": "mocha --exit",
Expand Down
31 changes: 21 additions & 10 deletions src/runtime/internal/lifecycle.ts
Expand Up @@ -56,6 +56,14 @@ export function onDestroy(fn: () => any) {
get_current_component().$$.on_destroy.push(fn);
}

export interface EventDispatcher<EventMap extends Record<string, any>> {
<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] :
undefined extends EventMap[Type] ? [type: Type, parameter?: EventMap[Type], options?: DispatchOptions] :
[type: Type, parameter: EventMap[Type], options?: DispatchOptions]): boolean;
}

export interface DispatchOptions {
cancelable?: boolean;
}
Expand All @@ -68,20 +76,23 @@ export interface DispatchOptions {
* [CustomEvent](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent).
* These events do not [bubble](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events#Event_bubbling_and_capture).
* The `detail` argument corresponds to the [CustomEvent.detail](https://developer.mozilla.org/en-US/docs/Web/API/CustomEvent/detail)
* property and can contain any type of data.
* property and can contain any type of data.
*
* The event dispatcher can be typed to narrow the allowed event names and the type of the `detail` argument:
* ```ts
* const dispatch = createEventDispatcher<{
* loaded: never; // does not take a detail argument
* change: string; // takes a detail argument of type string, which is required
* optional: number | null; // takes an optional detail argument of type number
* }>();
* ```
*
* https://svelte.dev/docs#run-time-svelte-createeventdispatcher
*/
export function createEventDispatcher<EventMap extends {} = any>(): <
EventKey extends Extract<keyof EventMap, string>
>(
type: EventKey,
detail?: EventMap[EventKey],
options?: DispatchOptions
) => boolean {
export function createEventDispatcher<EventMap extends Record<string, any> = any>(): EventDispatcher<EventMap> {
const component = get_current_component();

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

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

return true;
};
}) as EventDispatcher<EventMap>;
}

/**
Expand Down
43 changes: 43 additions & 0 deletions test/types/create-event-dispatcher.ts
@@ -0,0 +1,43 @@
import { createEventDispatcher } from '$runtime/internal/lifecycle';

const dispatch = createEventDispatcher<{
loaded: never
change: string
valid: boolean
optional: number | null
}>();

// @ts-expect-error: dispatch invalid event
dispatch('some-event');

dispatch('loaded');
dispatch('loaded', null);
dispatch('loaded', undefined);
dispatch('loaded', undefined, { cancelable: true });
// @ts-expect-error: no detail accepted
dispatch('loaded', 123);

// @ts-expect-error: detail not provided
dispatch('change');
dispatch('change', 'string');
dispatch('change', 'string', { cancelable: true });
// @ts-expect-error: wrong type of detail
dispatch('change', 123);
// @ts-expect-error: wrong type of detail
dispatch('change', undefined);

dispatch('valid', true);
dispatch('valid', true, { cancelable: true });
// @ts-expect-error: wrong type of detail
dispatch('valid', 'string');

dispatch('optional');
dispatch('optional', 123);
dispatch('optional', 123, { cancelable: true });
dispatch('optional', null);
dispatch('optional', undefined);
dispatch('optional', undefined, { cancelable: true });
// @ts-expect-error: wrong type of optional detail
dispatch('optional', 'string');
// @ts-expect-error: wrong type of option
dispatch('optional', undefined, { cancelabled: true });
16 changes: 16 additions & 0 deletions test/types/tsconfig.json
@@ -0,0 +1,16 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"rootDir": "../..",
"baseUrl": "../../",
"paths": {
"$runtime/*": ["src/runtime/*"]
},
// enable strictest options
"allowUnreachableCode": false,
"noFallthroughCasesInSwitch": true,
"noImplicitReturns": true,
"strict": true,
},
"include": ["."]
}

0 comments on commit d6bcddd

Please sign in to comment.