Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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