From f076646214760fd0701e6bad3edf443858a23503 Mon Sep 17 00:00:00 2001 From: Simon Holthausen Date: Wed, 3 Apr 2024 10:38:37 +0200 Subject: [PATCH] feat: provide ContextKey type for better typing of `setContext/getContext` Not completly ideal because you can circumvent the type safety by doing `getContext(context_key)` - changing this would require a breaking change, which we could do in Svelte 6 after we've given `ContextKey` some time to establish itself. Also doesn't add the interesting type narrowing idea in https://github.com/KamenKolev/svelte-typed-context/blob/master/index.ts#L14 (yet), probably easier to do together with said breaking change. closes #8941 --- .changeset/cold-fireants-report.md | 5 +++ packages/svelte/src/index.d.ts | 11 ++++++ .../svelte/src/internal/client/runtime.js | 14 ++++--- .../svelte/src/internal/client/types.d.ts | 6 +++ packages/svelte/src/internal/types.d.ts | 3 ++ packages/svelte/tests/types/context.ts | 39 +++++++++++++++++++ packages/svelte/types/index.d.ts | 21 +++++++++- 7 files changed, 91 insertions(+), 8 deletions(-) create mode 100644 .changeset/cold-fireants-report.md create mode 100644 packages/svelte/tests/types/context.ts diff --git a/.changeset/cold-fireants-report.md b/.changeset/cold-fireants-report.md new file mode 100644 index 000000000..1f88e86bb --- /dev/null +++ b/.changeset/cold-fireants-report.md @@ -0,0 +1,5 @@ +--- +"svelte": patch +--- + +feat: provide ContextKey type for better typing of `setContext/getContext` diff --git a/packages/svelte/src/index.d.ts b/packages/svelte/src/index.d.ts index bebe69a9b..dbd3bc1ff 100644 --- a/packages/svelte/src/index.d.ts +++ b/packages/svelte/src/index.d.ts @@ -222,5 +222,16 @@ export interface EventDispatcher> { ): boolean; } +/** + * Can be used to type `getContext`/`setContext`: + * ```ts + * import { getContext, setContext, type ContextKey } from 'svelte'; + * const context_key: ContextKey = Symbol('my boolean context key'); + * setContext(context_key, true); + * const value = getContext(context_key); // infered as boolean | undefined + * ``` + */ +export interface ContextKey extends Symbol {} + export * from './index-client.js'; import './ambient.js'; diff --git a/packages/svelte/src/internal/client/runtime.js b/packages/svelte/src/internal/client/runtime.js index 28428c1fb..70d4babf3 100644 --- a/packages/svelte/src/internal/client/runtime.js +++ b/packages/svelte/src/internal/client/runtime.js @@ -871,12 +871,13 @@ export function is_signal(val) { * * https://svelte.dev/docs/svelte#getcontext * @template T - * @param {any} key - * @returns {T} + * @template [Key=any] + * @param {Key} key + * @returns {import('./types.js').ContextType} */ export function getContext(key) { const context_map = get_or_init_context_map(); - const result = /** @type {T} */ (context_map.get(key)); + const result = /** @type {any} */ (context_map.get(key)); if (DEV) { // @ts-expect-error @@ -898,9 +899,10 @@ export function getContext(key) { * * https://svelte.dev/docs/svelte#setcontext * @template T - * @param {any} key - * @param {T} context - * @returns {T} + * @template [Key=any] + * @param {Key} key + * @param {import('./types.js').ContextType} context + * @returns {import('./types.js').ContextType} */ export function setContext(key, context) { const context_map = get_or_init_context_map(); diff --git a/packages/svelte/src/internal/client/types.d.ts b/packages/svelte/src/internal/client/types.d.ts index 1991769ee..2c30f1e20 100644 --- a/packages/svelte/src/internal/client/types.d.ts +++ b/packages/svelte/src/internal/client/types.d.ts @@ -1,4 +1,6 @@ import type { Store } from '#shared'; +import type { ContextKey } from 'svelte'; +import type { IsAny } from '../types.js'; import { STATE_SYMBOL } from './constants.js'; import type { Effect, Source, Value } from './reactivity/types.js'; @@ -169,4 +171,8 @@ export type ProxyStateObject> = T & { [STATE_SYMBOL]: ProxyMetadata; }; +export type ContextType = + // We need to specifically check for `any` because else it satisfies both conditions which results in the type being `unknown` + IsAny extends true ? T : Key extends ContextKey ? X | undefined : T; + export * from './reactivity/types'; diff --git a/packages/svelte/src/internal/types.d.ts b/packages/svelte/src/internal/types.d.ts index 12b2e5d4f..5c67afbf6 100644 --- a/packages/svelte/src/internal/types.d.ts +++ b/packages/svelte/src/internal/types.d.ts @@ -1,2 +1,5 @@ /** Anything except a function */ export type NotFunction = T extends Function ? never : T; + +/** Helper function to detect `any` */ +export type IsAny = 0 extends 1 & T ? true : false; diff --git a/packages/svelte/tests/types/context.ts b/packages/svelte/tests/types/context.ts new file mode 100644 index 000000000..00626e2bd --- /dev/null +++ b/packages/svelte/tests/types/context.ts @@ -0,0 +1,39 @@ +import { getContext, setContext, type ContextKey } from 'svelte'; + +const context_key: ContextKey = Symbol('foo'); +// @ts-expect-error +const context_key_wrong: ContextKey = true; + +setContext(context_key, true); +// @ts-expect-error +setContext(context_key, ''); + +const ok_1: boolean | undefined = getContext(context_key); +const sadly_ok: string = getContext(context_key); // making this an error at some point would be good; requires a breaking change +// @ts-expect-error +const not_ok_1: boolean = getContext(context_key); +// @ts-expect-error +const not_ok_2: string = getContext(context_key); + +const any_key: any = {}; + +setContext(any_key, true); + +const ok_2: boolean = getContext(any_key); +const ok_3: string = getContext(any_key); +const ok_4: string = getContext(any_key); +// @ts-expect-error +const not_ok_3: string = getContext(any_key); + +const boolean_key = true; + +setContext(boolean_key, true); +setContext(boolean_key, true); +// @ts-expect-error +setContext(boolean_key, ''); + +const ok_5: boolean = getContext(boolean_key); +const ok_6: string = getContext(boolean_key); +const ok_7: string = getContext(boolean_key); +// @ts-expect-error +const not_ok_4: string = getContext(boolean_key); diff --git a/packages/svelte/types/index.d.ts b/packages/svelte/types/index.d.ts index 00b318b8f..e3b4514a1 100644 --- a/packages/svelte/types/index.d.ts +++ b/packages/svelte/types/index.d.ts @@ -222,6 +222,17 @@ declare module 'svelte' { : [type: Type, parameter: EventMap[Type], options?: DispatchOptions] ): boolean; } + + /** + * Can be used to type `getContext`/`setContext`: + * ```ts + * import { getContext, setContext, type ContextKey } from 'svelte'; + * const context_key: ContextKey = Symbol('my boolean context key'); + * setContext(context_key, true); + * const value = getContext(context_key); // infered as boolean | undefined + * ``` + */ + export interface ContextKey extends Symbol {} /** * The `onMount` function schedules a callback to run as soon as the component has been mounted to the DOM. * It must be called during the component's initialisation (but doesn't need to live *inside* the component; @@ -293,6 +304,9 @@ declare module 'svelte' { export function flushSync(fn?: (() => void) | undefined): void; /** Anything except a function */ type NotFunction = T extends Function ? never : T; + + /** Helper function to detect `any` */ + type IsAny = 0 extends 1 & T ? true : false; export function unstate(value: T): T; /** * Mounts a component to the given target and returns the exports and potentially the props (if compiled with `accessors: true`) of the component @@ -337,7 +351,7 @@ declare module 'svelte' { * * https://svelte.dev/docs/svelte#getcontext * */ - export function getContext(key: any): T; + export function getContext(key: Key): ContextType; /** * Associates an arbitrary `context` object with the current component and the specified `key` * and returns that object. The context is then available to children of the component @@ -347,7 +361,7 @@ declare module 'svelte' { * * https://svelte.dev/docs/svelte#setcontext * */ - export function setContext(key: any, context: T): T; + export function setContext(key: Key, context: ContextType): ContextType; /** * Checks whether a given `key` has been set in the context of a parent component. * Must be called during component initialisation. @@ -363,6 +377,9 @@ declare module 'svelte' { * https://svelte.dev/docs/svelte#getallcontexts * */ export function getAllContexts = Map>(): T; + type ContextType = + // We need to specifically check for `any` because else it satisfies both conditions which results in the type being `unknown` + IsAny extends true ? T : Key extends ContextKey ? X | undefined : T; } declare module 'svelte/action' {