Skip to content

Commit 4b5272f

Browse files
fix(types): boundary primitives return a typed Accessor<T | U> at every layer
createErrorBoundary/createLoadingBoundary previously returned () => unknown (or leaked SourceAccessor<unknown>), so reading a boundary's value required casts and surfaced as TS warnings in every rollup build. The external signature is now identical across @solidjs/signals core, solid-js client hydration, and the server runtime: pass content and fallback functions, get back Accessor<T | U>. The SSR loading boundary's template plumbing moves to a private ssrLoadingBoundary cast at a single point, since its accessor yields resolved fragments rather than T/U. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 73c5913 commit 4b5272f

5 files changed

Lines changed: 58 additions & 38 deletions

File tree

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@solidjs/signals": patch
3+
"solid-js": patch
4+
---
5+
6+
`createErrorBoundary` and `createLoadingBoundary` now return a properly typed `Accessor<T | U>` (content union fallback) instead of `() => unknown`, with the same external signature across the core, client hydration, and server layers.

packages/solid-signals/src/boundaries.ts

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -368,10 +368,10 @@ export class CollectionQueue extends Queue {
368368

369369
function createCollectionBoundary<T>(
370370
type: number,
371-
fn: () => any,
372-
fallback: (queue: CollectionQueue) => any,
371+
fn: () => T,
372+
fallback: (queue: CollectionQueue) => T,
373373
onFn?: () => any
374-
) {
374+
): Accessor<T> {
375375
if (__DEV__ && !getOwner()) {
376376
const message =
377377
"[NO_OWNER_BOUNDARY] Boundaries created outside a reactive context will never be disposed.";
@@ -410,14 +410,17 @@ function createCollectionBoundary<T>(
410410
controller.register(queue);
411411
cleanup(() => controller.unregister(queue));
412412
}
413-
return accessor(
413+
return accessor<T>(
414414
computed(
415-
() => {
415+
(): T => {
416416
if (!read(queue._disabled)) {
417417
const resolved = read(tree);
418418
if (!untrack(() => read(queue._disabled))) return ((queue._initialized = true), resolved);
419419
}
420-
if (_revealUsed && read(queue._collapsed)) return undefined;
420+
// Collapsed reveal slots suppress their own output entirely; the
421+
// renderer treats the hole as empty, so the cast never leaks to users
422+
// outside a `createRevealOrder` scope.
423+
if (_revealUsed && read(queue._collapsed)) return undefined as T;
421424
return fallback(queue);
422425
},
423426
// Boundary structure, not a user source: its value is fallback-or-content and
@@ -452,12 +455,12 @@ function createCollectionBoundary<T>(
452455
* }
453456
* ```
454457
*/
455-
export function createLoadingBoundary(
456-
fn: () => any,
457-
fallback: () => any,
458+
export function createLoadingBoundary<T, U>(
459+
fn: () => T,
460+
fallback: () => U,
458461
options?: { on?: () => any }
459-
) {
460-
return createCollectionBoundary(STATUS_PENDING, fn, () => fallback(), options?.on);
462+
): Accessor<T | U> {
463+
return createCollectionBoundary<T | U>(STATUS_PENDING, fn, () => fallback(), options?.on);
461464
}
462465

463466
/**
@@ -483,11 +486,11 @@ export function createLoadingBoundary(
483486
* }
484487
* ```
485488
*/
486-
export function createErrorBoundary<U>(
487-
fn: () => any,
489+
export function createErrorBoundary<T, U>(
490+
fn: () => T,
488491
fallback: (error: Accessor<unknown>, reset: () => void) => U
489-
) {
490-
return createCollectionBoundary(STATUS_ERROR, fn, queue => {
492+
): Accessor<T | U> {
493+
return createCollectionBoundary<T | U>(STATUS_ERROR, fn, queue => {
491494
return fallback(accessor(queue._error!), () => {
492495
for (const source of queue._sources) recompute(source);
493496
schedule();

packages/solid/src/client/hydration.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -583,10 +583,10 @@ function hydratedCreateSignal(fn?: any, second?: any) {
583583
return coreSignal((prev: any) => readSerializedOrCompute(fn, prev), second);
584584
}
585585

586-
function hydratedCreateErrorBoundary<U>(
587-
fn: () => any,
586+
function hydratedCreateErrorBoundary<T, U>(
587+
fn: () => T,
588588
fallback: (error: () => unknown, reset: () => void) => U
589-
): () => unknown {
589+
): Accessor<T | U> {
590590
if (!sharedConfig.hydrating) return coreErrorBoundary(fn, fallback);
591591
markTopLevelSnapshotScope();
592592
const parent = getOwner()!;
@@ -922,10 +922,10 @@ export const createSignal: {
922922
* @internal
923923
*/
924924
export const createErrorBoundary = ((...args: any[]) =>
925-
(_createErrorBoundary || coreErrorBoundary)(...args)) as <U>(
926-
fn: () => any,
925+
(_createErrorBoundary || coreErrorBoundary)(...args)) as <T, U>(
926+
fn: () => T,
927927
fallback: (error: Accessor<unknown>, reset: () => void) => U
928-
) => () => unknown;
928+
) => Accessor<T | U>;
929929

930930
/**
931931
* Internal primitive that backs `<Reveal>` coordination of sibling loading
@@ -1362,11 +1362,11 @@ function scheduleResumeAfterAssets(
13621362
*
13631363
* @internal
13641364
*/
1365-
export function createLoadingBoundary(
1366-
fn: () => any,
1367-
fallback: () => any,
1365+
export function createLoadingBoundary<T, U>(
1366+
fn: () => T,
1367+
fallback: () => U,
13681368
options?: { on?: () => any }
1369-
): () => unknown {
1369+
): Accessor<T | U> {
13701370
if (!sharedConfig.hydrating) return coreLoadingBoundary(fn, fallback, options);
13711371

13721372
let settledSerializationResumeQueued = false;
@@ -1494,7 +1494,7 @@ export function createLoadingBoundary(
14941494
return undefined;
14951495
}
14961496
return coreLoadingBoundary(fn, fallback, options);
1497-
}) as unknown as () => unknown;
1497+
}) as unknown as Accessor<T | U>;
14981498
}
14991499

15001500
/**

packages/solid/src/server/hydration.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {
1212
} from "./signals.js";
1313
import { sharedConfig, NoHydrateContext } from "./shared.js";
1414
import type { SSRTemplateObject, HydrationContext } from "./shared.js";
15-
import type { Context } from "./signals.js";
15+
import type { Accessor, Context } from "./signals.js";
1616
import type { Element as SolidElement } from "../types.js";
1717

1818
export { sharedConfig, NoHydrateContext } from "./shared.js";
@@ -65,15 +65,26 @@ export function ssrHandleError(err: any) {
6565
throw err;
6666
}
6767

68-
export function createLoadingBoundary(
69-
fn: () => any,
70-
fallback: () => any,
68+
export function createLoadingBoundary<T, U>(
69+
fn: () => T,
70+
fallback: () => U,
7171
options?: { on?: () => any }
72-
): () => unknown {
72+
): Accessor<T | U> {
7373
const currentCtx = sharedConfig.context;
7474
if (!currentCtx) {
7575
return coreLoadingBoundary(fn, fallback);
7676
}
77+
// Under an SSR context the accessor yields resolved template fragments, not
78+
// T/U — the declared signature is the isomorphic contract the renderer and
79+
// client share; the SSR plumbing below is cast to it.
80+
return ssrLoadingBoundary(currentCtx, fn, fallback) as unknown as Accessor<T | U>;
81+
}
82+
83+
function ssrLoadingBoundary(
84+
currentCtx: HydrationContext,
85+
fn: () => any,
86+
fallback: () => any
87+
): () => unknown {
7788
const ctx = currentCtx;
7889
const parent = getOwner();
7990
const parentHandler = parent && runWithOwner(parent, () => getContext(ErrorContext));

packages/solid/src/server/signals.ts

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1584,10 +1584,10 @@ export function runWithBoundaryErrorContext<T>(
15841584

15851585
export { NoHydrateContext };
15861586

1587-
export function createErrorBoundary<U>(
1588-
fn: () => any,
1587+
export function createErrorBoundary<T, U>(
1588+
fn: () => T,
15891589
fallback: (error: Accessor<unknown>, reset: () => void) => U
1590-
): () => unknown {
1590+
): Accessor<T | U> {
15911591
const ctx = sharedConfig.context;
15921592
const parent = getOwner();
15931593
const owner = createOwner();
@@ -1653,11 +1653,11 @@ export function createErrorBoundary<U>(
16531653
};
16541654
}
16551655

1656-
export function createLoadingBoundary(
1657-
fn: () => any,
1658-
fallback: () => any,
1656+
export function createLoadingBoundary<T, U>(
1657+
fn: () => T,
1658+
fallback: () => U,
16591659
options?: { on?: () => any }
1660-
): () => unknown {
1660+
): Accessor<T | U> {
16611661
// On server, try to run fn. If NotReadyError is thrown, return fallback.
16621662
// Full HydrationContext integration happens in the Loading component wrapper.
16631663
try {

0 commit comments

Comments
 (0)