diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 9288f8333c0b..8520c0431033 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 44b50c6951d3..a6345d99b317 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/package.json b/package.json index c5239c8b502e..dc27fa208462 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/runtime/internal/lifecycle.ts b/src/runtime/internal/lifecycle.ts index e75bbdc501f4..7592e72a3860 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -56,6 +56,14 @@ export function onDestroy(fn: () => any) { get_current_component().$$.on_destroy.push(fn); } +export interface EventDispatcher> { + ( + ...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; } @@ -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(): < - EventKey extends Extract ->( - type: EventKey, - detail?: EventMap[EventKey], - options?: DispatchOptions -) => boolean { +export function createEventDispatcher = any>(): EventDispatcher { 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) { @@ -95,7 +106,7 @@ export function createEventDispatcher(): < } return true; - }; + }) as EventDispatcher; } /** diff --git a/test/types/create-event-dispatcher.ts b/test/types/create-event-dispatcher.ts new file mode 100644 index 000000000000..d9fc6c65bdce --- /dev/null +++ b/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 }); diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 000000000000..108ed2a2b29a --- /dev/null +++ b/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": ["."] +} \ No newline at end of file