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

feat: improve types for createEventDispatcher #7224

Merged
merged 15 commits into from Apr 14, 2023
Merged
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": ["."]
}