From 8d5767497e1801c8b2cc9cd21d89b72743918cef Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Fri, 14 Apr 2023 11:44:07 +0200 Subject: [PATCH] simplify type, add tests --- package.json | 2 +- src/runtime/internal/lifecycle.ts | 34 ++++++++++----------- test/types/create-event-dispatcher.ts | 43 +++++++++++++++++++++++++++ test/types/tsconfig.json | 16 ++++++++++ 4 files changed, 76 insertions(+), 19 deletions(-) create mode 100644 test/types/create-event-dispatcher.ts create mode 100644 test/types/tsconfig.json diff --git a/package.json b/package.json index c5239c8b50..8b69a2f15e 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 fb4048f52f..7592e72a38 100644 --- a/src/runtime/internal/lifecycle.ts +++ b/src/runtime/internal/lifecycle.ts @@ -56,25 +56,14 @@ export function onDestroy(fn: () => any) { get_current_component().$$.on_destroy.push(fn); } -type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void - ? I - : never - -type ExtractObjectValues> = Object[keyof Object] - -type ConstructDispatchFunction, EventKey extends keyof EventMap> = - EventMap[EventKey] extends never | null - ? (type: EventKey, detail?: null, options?: DispatchOptions) => boolean - : null extends EventMap[EventKey] - ? (type: EventKey, detail?: EventMap[EventKey], options?: DispatchOptions) => boolean - : (type: EventKey, detail: EventMap[EventKey], options?: DispatchOptions) => boolean - -type CreateDispatchFunctionMap = { - [Key in keyof EventMap]: ConstructDispatchFunction +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; } -type EventDispatcher> = UnionToIntersection>> - export interface DispatchOptions { cancelable?: boolean; } @@ -87,7 +76,16 @@ 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 */ diff --git a/test/types/create-event-dispatcher.ts b/test/types/create-event-dispatcher.ts new file mode 100644 index 0000000000..d9fc6c65bd --- /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 0000000000..108ed2a2b2 --- /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