feat: provide ContextKey type for better typing of `setContext/getContext`

Not completly ideal because you can circumvent the type safety by doing `getContext<SomeType>(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
typed-context
Simon Holthausen 3 months ago
parent d85d5a060b
commit f076646214

@ -0,0 +1,5 @@
---
"svelte": patch
---
feat: provide ContextKey type for better typing of `setContext/getContext`

@ -222,5 +222,16 @@ export interface EventDispatcher<EventMap extends Record<string, any>> {
): boolean;
}
/**
* Can be used to type `getContext`/`setContext`:
* ```ts
* import { getContext, setContext, type ContextKey } from 'svelte';
* const context_key: ContextKey<boolean> = Symbol('my boolean context key');
* setContext(context_key, true);
* const value = getContext(context_key); // infered as boolean | undefined
* ```
*/
export interface ContextKey<T> extends Symbol {}
export * from './index-client.js';
import './ambient.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<T, Key>}
*/
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<T, Key>} context
* @returns {import('./types.js').ContextType<T, Key>}
*/
export function setContext(key, context) {
const context_map = get_or_init_context_map();

@ -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 = Record<string | symbol, any>> = T & {
[STATE_SYMBOL]: ProxyMetadata;
};
export type ContextType<T, Key> =
// We need to specifically check for `any` because else it satisfies both conditions which results in the type being `unknown`
IsAny<Key> extends true ? T : Key extends ContextKey<infer X> ? X | undefined : T;
export * from './reactivity/types';

@ -1,2 +1,5 @@
/** Anything except a function */
export type NotFunction<T> = T extends Function ? never : T;
/** Helper function to detect `any` */
export type IsAny<T> = 0 extends 1 & T ? true : false;

@ -0,0 +1,39 @@
import { getContext, setContext, type ContextKey } from 'svelte';
const context_key: ContextKey<boolean> = Symbol('foo');
// @ts-expect-error
const context_key_wrong: ContextKey<boolean> = true;
setContext(context_key, true);
// @ts-expect-error
setContext(context_key, '');
const ok_1: boolean | undefined = getContext(context_key);
const sadly_ok: string = getContext<string>(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<string>(any_key);
// @ts-expect-error
const not_ok_3: string = getContext<boolean>(any_key);
const boolean_key = true;
setContext(boolean_key, true);
setContext<boolean>(boolean_key, true);
// @ts-expect-error
setContext<boolean>(boolean_key, '');
const ok_5: boolean = getContext(boolean_key);
const ok_6: string = getContext(boolean_key);
const ok_7: string = getContext<string>(boolean_key);
// @ts-expect-error
const not_ok_4: string = getContext<boolean>(boolean_key);

@ -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<boolean> = Symbol('my boolean context key');
* setContext(context_key, true);
* const value = getContext(context_key); // infered as boolean | undefined
* ```
*/
export interface ContextKey<T> 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> = T extends Function ? never : T;
/** Helper function to detect `any` */
type IsAny<T> = 0 extends 1 & T ? true : false;
export function unstate<T>(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<T>(key: any): T;
export function getContext<T, Key = any>(key: Key): ContextType<T, Key>;
/**
* 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<T>(key: any, context: T): T;
export function setContext<T, Key = any>(key: Key, context: ContextType<T, Key>): ContextType<T, Key>;
/**
* 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<T extends Map<any, any> = Map<any, any>>(): T;
type ContextType<T, Key> =
// We need to specifically check for `any` because else it satisfies both conditions which results in the type being `unknown`
IsAny<Key> extends true ? T : Key extends ContextKey<infer X> ? X | undefined : T;
}
declare module 'svelte/action' {

Loading…
Cancel
Save