Skip to content

Commit

Permalink
feat: provide ContextKey type for better typing of `setContext/getCon…
Browse files Browse the repository at this point in the history
…text`

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
  • Loading branch information
dummdidumm committed Apr 3, 2024
1 parent d85d5a0 commit f076646
Show file tree
Hide file tree
Showing 7 changed files with 91 additions and 8 deletions.
5 changes: 5 additions & 0 deletions .changeset/cold-fireants-report.md
@@ -0,0 +1,5 @@
---
"svelte": patch
---

feat: provide ContextKey type for better typing of `setContext/getContext`
11 changes: 11 additions & 0 deletions packages/svelte/src/index.d.ts
Expand Up @@ -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';
14 changes: 8 additions & 6 deletions packages/svelte/src/internal/client/runtime.js
Expand Up @@ -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
Expand All @@ -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();
Expand Down
6 changes: 6 additions & 0 deletions 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';

Expand Down Expand Up @@ -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';
3 changes: 3 additions & 0 deletions packages/svelte/src/internal/types.d.ts
@@ -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;
39 changes: 39 additions & 0 deletions packages/svelte/tests/types/context.ts
@@ -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);
21 changes: 19 additions & 2 deletions packages/svelte/types/index.d.ts
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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' {
Expand Down

0 comments on commit f076646

Please sign in to comment.