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

TypeScript: Context.Provider value should not be assignable to undefined in the default case #1958

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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions packages/solid/src/reactive/signal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1130,10 +1130,10 @@ export function registerGraph(value: SourceMapValue): void {
export type ContextProviderComponent<T> = FlowComponent<{ value: T }>;

// Context API
export interface Context<T> {
export interface Context<T, D = T> {
id: symbol;
Provider: ContextProviderComponent<T>;
defaultValue: T;
defaultValue: D;
}

/**
Expand All @@ -1158,7 +1158,7 @@ export interface Context<T> {
export function createContext<T>(
defaultValue?: undefined,
options?: EffectOptions
): Context<T | undefined>;
): Context<T, undefined>;
export function createContext<T>(defaultValue: T, options?: EffectOptions): Context<T>;
export function createContext<T>(
defaultValue?: T,
Expand All @@ -1176,7 +1176,7 @@ export function createContext<T>(
*
* @description https://www.solidjs.com/docs/latest/api#usecontext
*/
export function useContext<T>(context: Context<T>): T {
export function useContext<T, D>(context: Context<T, D>): D extends undefined ? T | undefined : T {
return Owner && Owner.context && Owner.context[context.id] !== undefined
? Owner.context[context.id]
: context.defaultValue;
Expand Down Expand Up @@ -1215,7 +1215,7 @@ export type SuspenseContextType = {
resolved?: boolean;
};

type SuspenseContext = Context<SuspenseContextType | undefined> & {
type SuspenseContext = Context<SuspenseContextType, undefined> & {
active?(): boolean;
increment?(): void;
decrement?(): void;
Expand All @@ -1224,7 +1224,7 @@ type SuspenseContext = Context<SuspenseContextType | undefined> & {
let SuspenseContext: SuspenseContext;

export function getSuspenseContext() {
return SuspenseContext || (SuspenseContext = createContext<SuspenseContextType | undefined>());
return SuspenseContext || (SuspenseContext = createContext<SuspenseContextType>());
}

// Interop
Expand Down
85 changes: 72 additions & 13 deletions packages/solid/web/test/context.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -20,13 +20,14 @@ describe("Testing Context", () => {
</Show>
);
};
const div = document.createElement("div");

it("should create context properly", () => {
expect(ThemeContext.id).toBeDefined();
expect(ThemeContext.defaultValue).toBe("light");
});

it("should work with single provider child", () => {
const div = document.createElement("div");
render(
() => (
<ThemeContext.Provider value="dark">
Expand All @@ -35,11 +36,11 @@ describe("Testing Context", () => {
),
div
);
expect((div.firstChild as HTMLDivElement).innerHTML).toBe("dark");
div.innerHTML = "";
expect(div.children[0].innerHTML).toBe("dark");
});

it("should work with single conditional provider child", () => {
const div = document.createElement("div");
render(
() => (
<ThemeContext.Provider value="dark">
Expand All @@ -48,11 +49,11 @@ describe("Testing Context", () => {
),
div
);
expect((div.firstChild as HTMLDivElement).innerHTML).toBe("dark");
div.innerHTML = "";
expect(div.children[0].innerHTML).toBe("dark");
});

it("should work with multi provider child", () => {
const div = document.createElement("div");
render(
() => (
<ThemeContext.Provider value="dark">
Expand All @@ -62,11 +63,11 @@ describe("Testing Context", () => {
),
div
);
expect((div.firstChild!.nextSibling! as HTMLDivElement).innerHTML).toBe("dark");
div.innerHTML = "";
expect(div.children[1].innerHTML).toBe("dark");
});

it("should work with multi conditional provider child", () => {
const div = document.createElement("div");
render(
() => (
<ThemeContext.Provider value="dark">
Expand All @@ -76,11 +77,11 @@ describe("Testing Context", () => {
),
div
);
expect((div.firstChild!.nextSibling! as HTMLDivElement).innerHTML).toBe("dark");
div.innerHTML = "";
expect(div.children[1].innerHTML).toBe("dark");
});

it("should work with dynamic multi provider child", () => {
const div = document.createElement("div");
const child = () => <Component />;
render(
() => (
Expand All @@ -91,11 +92,11 @@ describe("Testing Context", () => {
),
div
);
expect((div.firstChild!.nextSibling! as HTMLDivElement).innerHTML).toBe("dark");
div.innerHTML = "";
expect(div.children[1].innerHTML).toBe("dark");
});

it("should work with dynamic multi conditional provider child", () => {
const div = document.createElement("div");
const child = () => <CondComponent />;
render(
() => (
Expand All @@ -106,7 +107,65 @@ describe("Testing Context", () => {
),
div
);
expect((div.firstChild!.nextSibling! as HTMLDivElement).innerHTML).toBe("dark");
div.innerHTML = "";
expect(div.children[1].innerHTML).toBe("dark");
});

const ThemeContextWithUndefined = createContext<string | undefined>("light");
const ComponentWithUndefined = () => {
const theme = useContext(ThemeContextWithUndefined);
// ?? 'undefined' will never get reached
return <div>{theme ?? "undefined"}</div>;
};

it("should override when nesting", () => {
const div = document.createElement("div");
render(
() => (
<>
<ComponentWithUndefined />
<ThemeContextWithUndefined.Provider value="dark">
<ComponentWithUndefined />
<ThemeContextWithUndefined.Provider value="darker">
<ComponentWithUndefined />
<ThemeContextWithUndefined.Provider value={undefined}>
<ComponentWithUndefined />
</ThemeContextWithUndefined.Provider>
</ThemeContextWithUndefined.Provider>
</ThemeContextWithUndefined.Provider>
</>
),
div
);
expect(div.children[0].innerHTML!).toBe("light");
expect(div.children[1].innerHTML!).toBe("dark");
expect(div.children[2].innerHTML!).toBe("darker");
expect(div.children[3].innerHTML!).toBe("light");
});

const ThemeContextWithoutDefault = createContext<string | undefined>();
const ComponentWithoutDefault = () => {
const theme = useContext(ThemeContextWithoutDefault);
return <div>{theme ?? "no-default"}</div>;
};

it("should work with no default provided", () => {
const div = document.createElement("div");
render(
() => (
<>
<ComponentWithoutDefault />
<ThemeContextWithoutDefault.Provider value="dark">
<ComponentWithoutDefault />
<ThemeContextWithoutDefault.Provider value={undefined}>
<ComponentWithoutDefault />
</ThemeContextWithoutDefault.Provider>
</ThemeContextWithoutDefault.Provider>
</>
),
div
);
expect(div.children[0].innerHTML!).toBe("no-default");
expect(div.children[1].innerHTML!).toBe("dark");
expect(div.children[2].innerHTML!).toBe("no-default");
});
});