From e0d8218325629b9ee3d2b1213490fbb9abcf1cf6 Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Fri, 13 Feb 2026 23:18:38 +0100 Subject: [PATCH 01/10] feat: add fiber type guards and assertion utilities Add type predicates for fiber tags (isHostComponent, isFunctionComponent, etc.) and assertion helpers for the fiber tree. --- src/fiber/typeGuards.ts | 615 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 615 insertions(+) create mode 100644 src/fiber/typeGuards.ts diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts new file mode 100644 index 0000000..6b27e93 --- /dev/null +++ b/src/fiber/typeGuards.ts @@ -0,0 +1,615 @@ +/* **************** */ +/* Type Guards and Assertions */ +/* **************** */ + +/** + * Type predicates and assertion functions for safe type narrowing. + * These replace explicit casts with runtime-validated type narrowing. + */ + +import type { + Effect, + Fiber, + FiberRoot, + Flags, + Hook, + Lane, + Lanes, + PortalStateNode, + UpdateQueue, +} from "./types"; +import { NoFlags, NoLanes, WorkTag } from "./types"; + +// ============================================ +// Fiber Tag Type Guards +// ============================================ + +/** + * Type guard for host component fibers. + * Narrows stateNode to Element. + */ +export function isHostComponentFiber( + fiber: Fiber, +): fiber is Fiber & { stateNode: Element } { + return fiber.tag === WorkTag.HostComponent; +} + +/** + * Type guard for host text fibers. + * Narrows stateNode to Text. + */ +export function isHostTextFiber( + fiber: Fiber, +): fiber is Fiber & { stateNode: Text } { + return fiber.tag === WorkTag.HostText; +} + +/** + * Type guard for host root fibers. + * Narrows stateNode to FiberRoot. + */ +export function isHostRootFiber( + fiber: Fiber, +): fiber is Fiber & { stateNode: FiberRoot } { + return fiber.tag === WorkTag.HostRoot; +} + +/** + * Type guard for portal fibers. + * Narrows stateNode to PortalStateNode. + */ +export function isHostPortalFiber( + fiber: Fiber, +): fiber is Fiber & { stateNode: PortalStateNode } { + return fiber.tag === WorkTag.HostPortal; +} + +/** + * Type guard for function component fibers. + */ +export function isFunctionComponentFiber(fiber: Fiber): boolean { + return fiber.tag === WorkTag.FunctionComponent; +} + +/** + * Type guard for memo component fibers. + */ +export function isMemoComponentFiber(fiber: Fiber): boolean { + return fiber.tag === WorkTag.MemoComponent; +} + +/** + * Type guard for fragment fibers. + */ +export function isFragmentFiber(fiber: Fiber): boolean { + return fiber.tag === WorkTag.Fragment; +} + +/** + * Type guard for context provider fibers. + */ +export function isContextProviderFiber(fiber: Fiber): boolean { + return fiber.tag === WorkTag.ContextProvider; +} + +/** + * Type guard for context consumer fibers. + */ +export function isContextConsumerFiber(fiber: Fiber): boolean { + return fiber.tag === WorkTag.ContextConsumer; +} + +// ============================================ +// StateNode Assertions (using `asserts` for type narrowing) +// ============================================ + +/** + * Narrowed fiber type for host components with non-null stateNode. + */ +export type HostComponentFiber = Fiber & { + tag: typeof WorkTag.HostComponent; + stateNode: Element; +}; + +/** + * Narrowed fiber type for host text with non-null stateNode. + */ +export type HostTextFiber = Fiber & { + tag: typeof WorkTag.HostText; + stateNode: Text; +}; + +/** + * Narrowed fiber type for host root with non-null stateNode. + */ +export type HostRootFiber = Fiber & { + tag: typeof WorkTag.HostRoot; + stateNode: FiberRoot; +}; + +/** + * Narrowed fiber type for portal with non-null stateNode. + */ +export type HostPortalFiber = Fiber & { + tag: typeof WorkTag.HostPortal; + stateNode: PortalStateNode; +}; + +/** + * Asserts that a fiber is a host component with non-null stateNode. + * Narrows the type in the calling scope. + * @throws Error if stateNode is null or fiber is not a host component + */ +export function assertHostComponentFiber( + fiber: Fiber, +): asserts fiber is HostComponentFiber { + if (fiber.tag !== WorkTag.HostComponent) { + throw new Error(`Expected HostComponent fiber, got tag ${fiber.tag}`); + } + if (fiber.stateNode === null) { + throw new Error("HostComponent fiber has null stateNode"); + } +} + +/** + * Asserts that a fiber is a host text with non-null stateNode. + * Narrows the type in the calling scope. + * @throws Error if stateNode is null or fiber is not a host text + */ +export function assertHostTextFiber( + fiber: Fiber, +): asserts fiber is HostTextFiber { + if (fiber.tag !== WorkTag.HostText) { + throw new Error(`Expected HostText fiber, got tag ${fiber.tag}`); + } + if (fiber.stateNode === null) { + throw new Error("HostText fiber has null stateNode"); + } +} + +/** + * Asserts that a fiber is a host root with non-null stateNode. + * Narrows the type in the calling scope. + * @throws Error if stateNode is null or fiber is not a host root + */ +export function assertHostRootFiber( + fiber: Fiber, +): asserts fiber is HostRootFiber { + if (fiber.tag !== WorkTag.HostRoot) { + throw new Error(`Expected HostRoot fiber, got tag ${fiber.tag}`); + } + if (fiber.stateNode === null) { + throw new Error("HostRoot fiber has null stateNode"); + } +} + +/** + * Asserts that a fiber is a portal with non-null stateNode. + * Narrows the type in the calling scope. + * @throws Error if stateNode is null or fiber is not a portal + */ +export function assertHostPortalFiber( + fiber: Fiber, +): asserts fiber is HostPortalFiber { + if (fiber.tag !== WorkTag.HostPortal) { + throw new Error(`Expected HostPortal fiber, got tag ${fiber.tag}`); + } + if (fiber.stateNode === null) { + throw new Error("HostPortal fiber has null stateNode"); + } +} + +/** + * Gets the stateNode as Element or Text for host fibers. + * Returns null if not a host fiber or stateNode is null. + */ +export function getHostStateNode(fiber: Fiber): Element | Text | null { + if (fiber.tag === WorkTag.HostComponent) { + return fiber.stateNode as Element | null; + } + if (fiber.tag === WorkTag.HostText) { + return fiber.stateNode as Text | null; + } + return null; +} + +/** + * Asserts fiber is a host fiber (component or text) with non-null stateNode. + * Narrows the type in the calling scope. + * @throws Error if not a host fiber or stateNode is null + */ +export function assertHostFiber( + fiber: Fiber, +): asserts fiber is HostComponentFiber | HostTextFiber { + if (fiber.tag !== WorkTag.HostComponent && fiber.tag !== WorkTag.HostText) { + throw new Error( + `Expected host fiber (HostComponent or HostText), got tag ${fiber.tag}`, + ); + } + if (fiber.stateNode === null) { + throw new Error("Host fiber has null stateNode"); + } +} + +// ============================================ +// MemoizedState Type Guards +// ============================================ + +/** + * Type guard for hook linked list in memoizedState. + */ +export function isHookState(value: unknown): value is Hook { + if (value === null || typeof value !== "object") { + return false; + } + const obj = value as Record; + return "memoizedState" in obj && "next" in obj && "queue" in obj; +} + +/** + * Type guard for effect in memoizedState. + */ +export function isEffectState(value: unknown): value is Effect { + if (value === null || typeof value !== "object") { + return false; + } + const obj = value as Record; + return "tag" in obj && "create" in obj && "deps" in obj; +} + +/** + * Asserts that memoizedState is a Hook and returns it. + * @throws Error if not a valid Hook + */ +export function assertHookState(fiber: Fiber): Hook { + const state = fiber.memoizedState; + if (!isHookState(state)) { + throw new Error("Expected Hook in fiber.memoizedState"); + } + return state; +} + +/** + * Gets memoized state as a specific type, with null check. + */ +export function getMemoizedState(fiber: Fiber): T | null { + return fiber.memoizedState as T | null; +} + +/** + * Asserts memoized state is not null and returns as type T. + * @throws Error if memoizedState is null + */ +export function assertMemoizedState(fiber: Fiber): T { + if (fiber.memoizedState === null) { + throw new Error("fiber.memoizedState is null"); + } + return fiber.memoizedState as T; +} + +// ============================================ +// Props Type Guards +// ============================================ + +/** + * Type for text element props. + */ +export type TextProps = { nodeValue: string | number }; + +/** + * Type guard for text props. + */ +export function isTextProps(props: unknown): props is TextProps { + if (props === null || typeof props !== "object") { + return false; + } + return "nodeValue" in props; +} + +/** + * Gets props as a record, with null handling. + */ +export function getPropsAsRecord(fiber: Fiber): Record | null { + const props = fiber.pendingProps ?? fiber.memoizedProps; + if (props === null || typeof props !== "object") { + return null; + } + return props as Record; +} + +/** + * Asserts props are not null and returns as Record. + * @throws Error if props are null + */ +export function assertPropsAsRecord(fiber: Fiber): Record { + const props = getPropsAsRecord(fiber); + if (props === null) { + throw new Error("Fiber props are null"); + } + return props; +} + +/** + * Asserts text props and returns them. + * @throws Error if not text props + */ +export function assertTextProps(fiber: Fiber): TextProps { + const props = fiber.pendingProps ?? fiber.memoizedProps; + if (!isTextProps(props)) { + throw new Error("Expected TextProps in fiber.pendingProps"); + } + return props; +} + +// ============================================ +// Update Queue Type Guards +// ============================================ + +/** + * Type guard for UpdateQueue. + */ +export function isUpdateQueue(value: unknown): value is UpdateQueue { + if (value === null || typeof value !== "object") { + return false; + } + const obj = value as Record; + return "pending" in obj && "lanes" in obj && "dispatch" in obj; +} + +/** + * Asserts update queue is not null and returns it. + * @throws Error if updateQueue is null + */ +export function assertUpdateQueue(hook: Hook): UpdateQueue { + if (hook.queue === null) { + throw new Error("Hook queue is null"); + } + return hook.queue as UpdateQueue; +} + +// ============================================ +// Branded Type Helpers +// ============================================ + +/** + * Safely performs bitwise OR on lanes. + */ +export function mergeLanesUnsafe(a: Lanes | Lane, b: Lanes | Lane): Lanes { + return ((a as number) | (b as number)) as Lanes; +} + +/** + * Safely performs bitwise AND on lanes. + */ +export function intersectLanesUnsafe(a: Lanes, b: Lanes): Lanes { + return ((a as number) & (b as number)) as Lanes; +} + +/** + * Safely performs bitwise AND NOT on lanes. + */ +export function removeLanesUnsafe(set: Lanes, subset: Lanes | Lane): Lanes { + return ((set as number) & ~(subset as number)) as Lanes; +} + +/** + * Checks if lanes include a specific lane. + */ +export function lanesIncludeLane(lanes: Lanes, lane: Lane): boolean { + return ((lanes as number) & (lane as number)) !== 0; +} + +/** + * Checks if lanes are empty. + */ +export function isLanesEmpty(lanes: Lanes): boolean { + return lanes === NoLanes; +} + +/** + * Checks if flags include a specific flag. + */ +export function flagsInclude(flags: Flags, flag: Flags): boolean { + return ((flags as number) & (flag as number)) !== 0; +} + +/** + * Merges flags. + */ +export function mergeFlags(a: Flags, b: Flags): Flags { + return ((a as number) | (b as number)) as Flags; +} + +/** + * Checks if flags are empty. + */ +export function isFlagsEmpty(flags: Flags): boolean { + return flags === NoFlags; +} + +// ============================================ +// DOM Type Guards +// ============================================ + +/** + * Type guard for Element. + */ +export function isElement(node: Node): node is Element { + return node.nodeType === Node.ELEMENT_NODE; +} + +/** + * Type guard for Text node. + */ +export function isTextNode(node: Node): node is Text { + return node.nodeType === Node.TEXT_NODE; +} + +/** + * Type guard for HTMLElement. + */ +export function isHTMLElement(node: Node): node is HTMLElement { + return node instanceof HTMLElement; +} + +/** + * Type guard for HTMLInputElement. + */ +export function isHTMLInputElement( + element: Element, +): element is HTMLInputElement { + return element instanceof HTMLInputElement; +} + +/** + * Asserts a node is an Element. + * Narrows the type in the calling scope. + * @throws Error if node is not an Element + */ +export function assertElement(node: Node): asserts node is Element { + if (!isElement(node)) { + throw new Error(`Expected Element, got nodeType ${node.nodeType}`); + } +} + +/** + * Asserts a node is a Text node. + * Narrows the type in the calling scope. + * @throws Error if node is not a Text node + */ +export function assertTextNode(node: Node): asserts node is Text { + if (!isTextNode(node)) { + throw new Error(`Expected Text node, got nodeType ${node.nodeType}`); + } +} + +// ============================================ +// FiberRoot Type Guards +// ============================================ + +/** + * Type guard for FiberRoot. + */ +export function isFiberRoot(value: unknown): value is FiberRoot { + if (value === null || typeof value !== "object") { + return false; + } + const obj = value as Record; + return "containerInfo" in obj && "current" in obj && "pendingLanes" in obj; +} + +/** + * Asserts a value is a FiberRoot. + * Narrows the type in the calling scope. + * @throws Error if not a FiberRoot + */ +export function assertFiberRoot(value: unknown): asserts value is FiberRoot { + if (!isFiberRoot(value)) { + throw new Error("Expected FiberRoot"); + } +} + +// ============================================ +// Fiber Type Guards +// ============================================ + +/** + * Type guard for Fiber with non-null return. + */ +export function hasFiberParent( + fiber: Fiber, +): fiber is Fiber & { return: Fiber } { + return fiber.return !== null; +} + +/** + * Type guard for Fiber with non-null child. + */ +export function hasFiberChild(fiber: Fiber): fiber is Fiber & { child: Fiber } { + return fiber.child !== null; +} + +/** + * Type guard for Fiber with non-null sibling. + */ +export function hasFiberSibling( + fiber: Fiber, +): fiber is Fiber & { sibling: Fiber } { + return fiber.sibling !== null; +} + +/** + * Type guard for Fiber with non-null alternate. + */ +export function hasFiberAlternate( + fiber: Fiber, +): fiber is Fiber & { alternate: Fiber } { + return fiber.alternate !== null; +} + +/** + * Asserts fiber has a parent and returns it. + * @throws Error if fiber.return is null + */ +export function assertFiberParent(fiber: Fiber): Fiber { + if (fiber.return === null) { + throw new Error("Fiber has no parent"); + } + return fiber.return; +} + +// ============================================ +// Safe Property Access +// ============================================ + +/** + * Safely sets a dynamic property on an object. + * Use this instead of `(obj as unknown as Record)[key] = value` + */ +export function setDynamicProperty( + obj: object, + key: string, + value: unknown, +): void { + (obj as Record)[key] = value; +} + +/** + * Safely gets a dynamic property from an object. + */ +export function getDynamicProperty(obj: object, key: string): T | undefined { + return (obj as Record)[key] as T | undefined; +} + +// ============================================ +// Component Type Guards +// ============================================ + +/** + * Type for a memo component with $$typeof symbol. + */ +export type MemoComponent = { + $$typeof: symbol; + type: unknown; + compare: ((prev: unknown, next: unknown) => boolean) | null; +}; + +/** + * Type guard for memo components. + */ +export function isMemoComponent(value: unknown): value is MemoComponent { + if (value === null || typeof value !== "object") { + return false; + } + const obj = value as Record; + return "$$typeof" in obj && typeof obj["$$typeof"] === "symbol"; +} + +/** + * Type guard for function component type. + */ +export function isFunctionType( + type: unknown, +): type is (props: Record) => unknown { + return typeof type === "function"; +} From 0ac7f6c0a7502c9082bd49f0dd4ed4abd9f161df Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Sun, 15 Feb 2026 16:59:50 +0100 Subject: [PATCH 02/10] fix: address review feedback --- src/fiber/typeGuards.ts | 38 ++++++++++++++++++++++++++------------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index 6b27e93..7fd24af 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -31,7 +31,7 @@ import { NoFlags, NoLanes, WorkTag } from "./types"; export function isHostComponentFiber( fiber: Fiber, ): fiber is Fiber & { stateNode: Element } { - return fiber.tag === WorkTag.HostComponent; + return fiber.tag === WorkTag.HostComponent && fiber.stateNode !== null; } /** @@ -41,7 +41,7 @@ export function isHostComponentFiber( export function isHostTextFiber( fiber: Fiber, ): fiber is Fiber & { stateNode: Text } { - return fiber.tag === WorkTag.HostText; + return fiber.tag === WorkTag.HostText && fiber.stateNode !== null; } /** @@ -51,7 +51,7 @@ export function isHostTextFiber( export function isHostRootFiber( fiber: Fiber, ): fiber is Fiber & { stateNode: FiberRoot } { - return fiber.tag === WorkTag.HostRoot; + return fiber.tag === WorkTag.HostRoot && fiber.stateNode !== null; } /** @@ -61,41 +61,51 @@ export function isHostRootFiber( export function isHostPortalFiber( fiber: Fiber, ): fiber is Fiber & { stateNode: PortalStateNode } { - return fiber.tag === WorkTag.HostPortal; + return fiber.tag === WorkTag.HostPortal && fiber.stateNode !== null; } /** * Type guard for function component fibers. */ -export function isFunctionComponentFiber(fiber: Fiber): boolean { +export function isFunctionComponentFiber( + fiber: Fiber, +): fiber is Fiber & { tag: typeof WorkTag.FunctionComponent } { return fiber.tag === WorkTag.FunctionComponent; } /** * Type guard for memo component fibers. */ -export function isMemoComponentFiber(fiber: Fiber): boolean { +export function isMemoComponentFiber( + fiber: Fiber, +): fiber is Fiber & { tag: typeof WorkTag.MemoComponent } { return fiber.tag === WorkTag.MemoComponent; } /** * Type guard for fragment fibers. */ -export function isFragmentFiber(fiber: Fiber): boolean { +export function isFragmentFiber( + fiber: Fiber, +): fiber is Fiber & { tag: typeof WorkTag.Fragment } { return fiber.tag === WorkTag.Fragment; } /** * Type guard for context provider fibers. */ -export function isContextProviderFiber(fiber: Fiber): boolean { +export function isContextProviderFiber( + fiber: Fiber, +): fiber is Fiber & { tag: typeof WorkTag.ContextProvider } { return fiber.tag === WorkTag.ContextProvider; } /** * Type guard for context consumer fibers. */ -export function isContextConsumerFiber(fiber: Fiber): boolean { +export function isContextConsumerFiber( + fiber: Fiber, +): fiber is Fiber & { tag: typeof WorkTag.ContextConsumer } { return fiber.tag === WorkTag.ContextConsumer; } @@ -303,7 +313,8 @@ export function isTextProps(props: unknown): props is TextProps { if (props === null || typeof props !== "object") { return false; } - return "nodeValue" in props; + const nv = (props as Record).nodeValue; + return typeof nv === "string" || typeof nv === "number"; } /** @@ -449,7 +460,7 @@ export function isTextNode(node: Node): node is Text { * Type guard for HTMLElement. */ export function isHTMLElement(node: Node): node is HTMLElement { - return node instanceof HTMLElement; + return typeof HTMLElement !== "undefined" && node instanceof HTMLElement; } /** @@ -458,7 +469,10 @@ export function isHTMLElement(node: Node): node is HTMLElement { export function isHTMLInputElement( element: Element, ): element is HTMLInputElement { - return element instanceof HTMLInputElement; + return ( + typeof HTMLInputElement !== "undefined" && + element instanceof HTMLInputElement + ); } /** From 732d6aff944c6f77e05b3ca14560f15fa9306e80 Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Sun, 15 Feb 2026 17:20:51 +0100 Subject: [PATCH 03/10] fix: address review comments --- src/fiber/typeGuards.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index 7fd24af..7416d2f 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -372,10 +372,10 @@ export function isUpdateQueue(value: unknown): value is UpdateQueue { * @throws Error if updateQueue is null */ export function assertUpdateQueue(hook: Hook): UpdateQueue { - if (hook.queue === null) { - throw new Error("Hook queue is null"); + if (!isUpdateQueue(hook.queue)) { + throw new Error("Hook queue is not a valid UpdateQueue"); } - return hook.queue as UpdateQueue; + return hook.queue; } // ============================================ @@ -446,14 +446,14 @@ export function isFlagsEmpty(flags: Flags): boolean { * Type guard for Element. */ export function isElement(node: Node): node is Element { - return node.nodeType === Node.ELEMENT_NODE; + return typeof Node !== "undefined" && node.nodeType === Node.ELEMENT_NODE; } /** * Type guard for Text node. */ export function isTextNode(node: Node): node is Text { - return node.nodeType === Node.TEXT_NODE; + return typeof Node !== "undefined" && node.nodeType === Node.TEXT_NODE; } /** @@ -616,7 +616,13 @@ export function isMemoComponent(value: unknown): value is MemoComponent { return false; } const obj = value as Record; - return "$$typeof" in obj && typeof obj["$$typeof"] === "symbol"; + return ( + "$$typeof" in obj && + typeof obj["$$typeof"] === "symbol" && + "type" in obj && + obj["type"] !== undefined && + (typeof obj["compare"] === "function" || obj["compare"] === null) + ); } /** From f7cc92d8eb33499416ef9d526d43e33cc9ba13ee Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Sun, 15 Feb 2026 17:32:24 +0100 Subject: [PATCH 04/10] chore: fix again - isUpdatedQueue now vlaidates all five UpdateQueue fields - isHookState now checks all five Hook properties --- src/fiber/typeGuards.ts | 29 ++++++++++++++++++++++++++--- 1 file changed, 26 insertions(+), 3 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index 7416d2f..2c6fdb2 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -253,7 +253,13 @@ export function isHookState(value: unknown): value is Hook { return false; } const obj = value as Record; - return "memoizedState" in obj && "next" in obj && "queue" in obj; + return ( + "memoizedState" in obj && + "baseState" in obj && + "baseQueue" in obj && + "queue" in obj && + "next" in obj + ); } /** @@ -264,7 +270,13 @@ export function isEffectState(value: unknown): value is Effect { return false; } const obj = value as Record; - return "tag" in obj && "create" in obj && "deps" in obj; + return ( + "tag" in obj && + "create" in obj && + "destroy" in obj && + "deps" in obj && + "next" in obj + ); } /** @@ -364,7 +376,18 @@ export function isUpdateQueue(value: unknown): value is UpdateQueue { return false; } const obj = value as Record; - return "pending" in obj && "lanes" in obj && "dispatch" in obj; + return ( + "pending" in obj && + (obj["pending"] === null || typeof obj["pending"] === "object") && + "lanes" in obj && + typeof obj["lanes"] === "number" && + "dispatch" in obj && + (obj["dispatch"] === null || typeof obj["dispatch"] === "function") && + "lastRenderedReducer" in obj && + (obj["lastRenderedReducer"] === null || + typeof obj["lastRenderedReducer"] === "function") && + "lastRenderedState" in obj + ); } /** From 9d9c9e15ac8768ec1fdd4f38bde06b70b44ce748 Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Sun, 15 Feb 2026 18:34:24 +0100 Subject: [PATCH 05/10] fix: address review feedback - setDynamicProperty / getDynamicProperty, both now reject __proto__, constructor, and prototype keys by throwing an error, preventing prototype pollution - isFiberRoot now validates types: containerInfo must be a non-null object - assertTextProps error message now correctly names the actual source (pendingProps or memoizedProps) based on which one was used - isHookState now validates that baseQueue, queue, and next are each either null or an object - isEffectState now validates that tag is a number, create is a function, destroy is undefined or a function, deps is null or an array, and next is null or an object. --- src/fiber/typeGuards.ts | 34 ++++++++++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 4 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index 2c6fdb2..39c1d09 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -257,8 +257,11 @@ export function isHookState(value: unknown): value is Hook { "memoizedState" in obj && "baseState" in obj && "baseQueue" in obj && + (obj["baseQueue"] === null || typeof obj["baseQueue"] === "object") && "queue" in obj && - "next" in obj + (obj["queue"] === null || typeof obj["queue"] === "object") && + "next" in obj && + (obj["next"] === null || typeof obj["next"] === "object") ); } @@ -272,10 +275,15 @@ export function isEffectState(value: unknown): value is Effect { const obj = value as Record; return ( "tag" in obj && + typeof obj["tag"] === "number" && "create" in obj && + typeof obj["create"] === "function" && "destroy" in obj && + (obj["destroy"] === undefined || typeof obj["destroy"] === "function") && "deps" in obj && - "next" in obj + (obj["deps"] === null || Array.isArray(obj["deps"])) && + "next" in obj && + (obj["next"] === null || typeof obj["next"] === "object") ); } @@ -359,7 +367,9 @@ export function assertPropsAsRecord(fiber: Fiber): Record { export function assertTextProps(fiber: Fiber): TextProps { const props = fiber.pendingProps ?? fiber.memoizedProps; if (!isTextProps(props)) { - throw new Error("Expected TextProps in fiber.pendingProps"); + const source = + fiber.pendingProps !== undefined ? "pendingProps" : "memoizedProps"; + throw new Error(`Expected TextProps in fiber.${source}`); } return props; } @@ -532,7 +542,13 @@ export function isFiberRoot(value: unknown): value is FiberRoot { return false; } const obj = value as Record; - return "containerInfo" in obj && "current" in obj && "pendingLanes" in obj; + return ( + obj["containerInfo"] !== null && + typeof obj["containerInfo"] === "object" && + obj["current"] !== null && + typeof obj["current"] === "object" && + typeof obj["pendingLanes"] === "number" + ); } /** @@ -599,8 +615,11 @@ export function assertFiberParent(fiber: Fiber): Fiber { // Safe Property Access // ============================================ +const UNSAFE_KEYS = new Set(["__proto__", "constructor", "prototype"]); + /** * Safely sets a dynamic property on an object. + * Rejects prototype-polluting keys (__proto__, constructor, prototype). * Use this instead of `(obj as unknown as Record)[key] = value` */ export function setDynamicProperty( @@ -608,13 +627,20 @@ export function setDynamicProperty( key: string, value: unknown, ): void { + if (UNSAFE_KEYS.has(key)) { + throw new Error(`Refusing to set unsafe property "${key}"`); + } (obj as Record)[key] = value; } /** * Safely gets a dynamic property from an object. + * Rejects prototype-polluting keys (__proto__, constructor, prototype). */ export function getDynamicProperty(obj: object, key: string): T | undefined { + if (UNSAFE_KEYS.has(key)) { + throw new Error(`Refusing to access unsafe property "${key}"`); + } return (obj as Record)[key] as T | undefined; } From ac6459c6c24176cda12a913cd84d7251c533be1d Mon Sep 17 00:00:00 2001 From: Marcel Olsen Date: Sun, 15 Feb 2026 19:50:19 +0100 Subject: [PATCH 06/10] fix: update typeGuards.ts --- src/fiber/typeGuards.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index 39c1d09..3350985 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -30,7 +30,7 @@ import { NoFlags, NoLanes, WorkTag } from "./types"; */ export function isHostComponentFiber( fiber: Fiber, -): fiber is Fiber & { stateNode: Element } { +): fiber is HostComponentFiber { return fiber.tag === WorkTag.HostComponent && fiber.stateNode !== null; } @@ -40,7 +40,7 @@ export function isHostComponentFiber( */ export function isHostTextFiber( fiber: Fiber, -): fiber is Fiber & { stateNode: Text } { +): fiber is HostTextFiber { return fiber.tag === WorkTag.HostText && fiber.stateNode !== null; } @@ -50,7 +50,7 @@ export function isHostTextFiber( */ export function isHostRootFiber( fiber: Fiber, -): fiber is Fiber & { stateNode: FiberRoot } { +): fiber is HostRootFiber { return fiber.tag === WorkTag.HostRoot && fiber.stateNode !== null; } @@ -60,7 +60,7 @@ export function isHostRootFiber( */ export function isHostPortalFiber( fiber: Fiber, -): fiber is Fiber & { stateNode: PortalStateNode } { +): fiber is HostPortalFiber { return fiber.tag === WorkTag.HostPortal && fiber.stateNode !== null; } From 5665e2f71196a9dfa8963e314f3b0ab408a57499 Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Sun, 15 Feb 2026 19:51:35 +0100 Subject: [PATCH 07/10] chore: format coderabbit suggestion --- src/fiber/typeGuards.ts | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index 3350985..40930c4 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -38,9 +38,7 @@ export function isHostComponentFiber( * Type guard for host text fibers. * Narrows stateNode to Text. */ -export function isHostTextFiber( - fiber: Fiber, -): fiber is HostTextFiber { +export function isHostTextFiber(fiber: Fiber): fiber is HostTextFiber { return fiber.tag === WorkTag.HostText && fiber.stateNode !== null; } @@ -48,9 +46,7 @@ export function isHostTextFiber( * Type guard for host root fibers. * Narrows stateNode to FiberRoot. */ -export function isHostRootFiber( - fiber: Fiber, -): fiber is HostRootFiber { +export function isHostRootFiber(fiber: Fiber): fiber is HostRootFiber { return fiber.tag === WorkTag.HostRoot && fiber.stateNode !== null; } @@ -58,9 +54,7 @@ export function isHostRootFiber( * Type guard for portal fibers. * Narrows stateNode to PortalStateNode. */ -export function isHostPortalFiber( - fiber: Fiber, -): fiber is HostPortalFiber { +export function isHostPortalFiber(fiber: Fiber): fiber is HostPortalFiber { return fiber.tag === WorkTag.HostPortal && fiber.stateNode !== null; } From 946f3f0019c21ab4b4886f8156328d874fe1d75e Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Sun, 15 Feb 2026 21:56:16 +0100 Subject: [PATCH 08/10] chore: review feedback --- src/fiber/typeGuards.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index 40930c4..aa4ebc5 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -251,9 +251,9 @@ export function isHookState(value: unknown): value is Hook { "memoizedState" in obj && "baseState" in obj && "baseQueue" in obj && - (obj["baseQueue"] === null || typeof obj["baseQueue"] === "object") && + (obj["baseQueue"] === null || isUpdateQueue(obj["baseQueue"])) && "queue" in obj && - (obj["queue"] === null || typeof obj["queue"] === "object") && + (obj["queue"] === null || isUpdateQueue(obj["queue"])) && "next" in obj && (obj["next"] === null || typeof obj["next"] === "object") ); @@ -362,7 +362,7 @@ export function assertTextProps(fiber: Fiber): TextProps { const props = fiber.pendingProps ?? fiber.memoizedProps; if (!isTextProps(props)) { const source = - fiber.pendingProps !== undefined ? "pendingProps" : "memoizedProps"; + fiber.pendingProps != null ? "pendingProps" : "memoizedProps"; throw new Error(`Expected TextProps in fiber.${source}`); } return props; From eaebf8721d71ffa75f56d6fef7c28dab334afbcd Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Sun, 15 Feb 2026 22:04:34 +0100 Subject: [PATCH 09/10] chore: more fixes --- src/fiber/typeGuards.ts | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index aa4ebc5..defd136 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -16,6 +16,7 @@ import type { Lane, Lanes, PortalStateNode, + Update, UpdateQueue, } from "./types"; import { NoFlags, NoLanes, WorkTag } from "./types"; @@ -251,7 +252,7 @@ export function isHookState(value: unknown): value is Hook { "memoizedState" in obj && "baseState" in obj && "baseQueue" in obj && - (obj["baseQueue"] === null || isUpdateQueue(obj["baseQueue"])) && + (obj["baseQueue"] === null || isUpdate(obj["baseQueue"])) && "queue" in obj && (obj["queue"] === null || isUpdateQueue(obj["queue"])) && "next" in obj && @@ -368,6 +369,30 @@ export function assertTextProps(fiber: Fiber): TextProps { return props; } +// ============================================ +// Update Type Guards +// ============================================ + +/** + * Type guard for Update. + */ +export function isUpdate(value: unknown): value is Update { + if (value === null || typeof value !== "object") { + return false; + } + const obj = value as Record; + return ( + "lane" in obj && + typeof obj["lane"] === "number" && + "action" in obj && + "hasEagerState" in obj && + typeof obj["hasEagerState"] === "boolean" && + "eagerState" in obj && + "next" in obj && + (obj["next"] === null || typeof obj["next"] === "object") + ); +} + // ============================================ // Update Queue Type Guards // ============================================ From adbf41b2d951b260e941c2d84f46ca6c5c54bee1 Mon Sep 17 00:00:00 2001 From: MarcelOlsen Date: Tue, 17 Feb 2026 18:51:57 +0100 Subject: [PATCH 10/10] refactor: strengthen fiber type guards with runtime validation and cleanup - Convert boolean-returning fiber tag helpers to real TS type predicates - Add stateNode !== null checks to host fiber type guards - Validate full shapes in isHookState, isEffectState, isUpdateQueue, and isMemoComponent instead of only checking key existence - Add isUpdate type guard for Update shape validation - Tighten isTextProps to validate nodeValue is string or number - Tighten isFiberRoot to verify property types, not just presence - Guard isHTMLElement, isHTMLInputElement, isElement, isTextNode against missing DOM globals for SSR safety - Fix assertTextProps error message to report correct prop source - Reject prototype-polluting keys in setDynamicProperty/getDynamicProperty - Add JSDoc warnings to getMemoizedState/assertMemoizedState about unchecked casts - Rename mergeLanesUnsafe/intersectLanesUnsafe/removeLanesUnsafe to mergeLanes/intersectLanes/removeLanes - Move Host* type aliases above their corresponding type guard functions --- src/fiber/typeGuards.ts | 92 +++++++++++++++++++++++------------------ 1 file changed, 51 insertions(+), 41 deletions(-) diff --git a/src/fiber/typeGuards.ts b/src/fiber/typeGuards.ts index defd136..9bd4eff 100644 --- a/src/fiber/typeGuards.ts +++ b/src/fiber/typeGuards.ts @@ -21,6 +21,42 @@ import type { } from "./types"; import { NoFlags, NoLanes, WorkTag } from "./types"; +// ============================================ +// Narrowed Fiber Type Aliases +// ============================================ + +/** + * Narrowed fiber type for host components with non-null stateNode. + */ +export type HostComponentFiber = Fiber & { + tag: typeof WorkTag.HostComponent; + stateNode: Element; +}; + +/** + * Narrowed fiber type for host text with non-null stateNode. + */ +export type HostTextFiber = Fiber & { + tag: typeof WorkTag.HostText; + stateNode: Text; +}; + +/** + * Narrowed fiber type for host root with non-null stateNode. + */ +export type HostRootFiber = Fiber & { + tag: typeof WorkTag.HostRoot; + stateNode: FiberRoot; +}; + +/** + * Narrowed fiber type for portal with non-null stateNode. + */ +export type HostPortalFiber = Fiber & { + tag: typeof WorkTag.HostPortal; + stateNode: PortalStateNode; +}; + // ============================================ // Fiber Tag Type Guards // ============================================ @@ -108,38 +144,6 @@ export function isContextConsumerFiber( // StateNode Assertions (using `asserts` for type narrowing) // ============================================ -/** - * Narrowed fiber type for host components with non-null stateNode. - */ -export type HostComponentFiber = Fiber & { - tag: typeof WorkTag.HostComponent; - stateNode: Element; -}; - -/** - * Narrowed fiber type for host text with non-null stateNode. - */ -export type HostTextFiber = Fiber & { - tag: typeof WorkTag.HostText; - stateNode: Text; -}; - -/** - * Narrowed fiber type for host root with non-null stateNode. - */ -export type HostRootFiber = Fiber & { - tag: typeof WorkTag.HostRoot; - stateNode: FiberRoot; -}; - -/** - * Narrowed fiber type for portal with non-null stateNode. - */ -export type HostPortalFiber = Fiber & { - tag: typeof WorkTag.HostPortal; - stateNode: PortalStateNode; -}; - /** * Asserts that a fiber is a host component with non-null stateNode. * Narrows the type in the calling scope. @@ -295,14 +299,20 @@ export function assertHookState(fiber: Fiber): Hook { } /** - * Gets memoized state as a specific type, with null check. + * Gets memoized state cast to a specific type, with null check. + * WARNING: Does not perform runtime validation of T. This is an unchecked cast. + * Use specific guards like {@link isHookState} or {@link isEffectState} when + * runtime validation is required. */ export function getMemoizedState(fiber: Fiber): T | null { return fiber.memoizedState as T | null; } /** - * Asserts memoized state is not null and returns as type T. + * Asserts memoized state is not null and returns cast to type T. + * WARNING: Does not perform runtime validation of T. This is an unchecked cast + * that only verifies non-null. Use specific guards like {@link isHookState} or + * {@link isEffectState} when runtime shape validation is required. * @throws Error if memoizedState is null */ export function assertMemoizedState(fiber: Fiber): T { @@ -407,7 +417,7 @@ export function isUpdateQueue(value: unknown): value is UpdateQueue { const obj = value as Record; return ( "pending" in obj && - (obj["pending"] === null || typeof obj["pending"] === "object") && + (obj["pending"] === null || isUpdate(obj["pending"])) && "lanes" in obj && typeof obj["lanes"] === "number" && "dispatch" in obj && @@ -435,23 +445,23 @@ export function assertUpdateQueue(hook: Hook): UpdateQueue { // ============================================ /** - * Safely performs bitwise OR on lanes. + * Performs bitwise OR on branded Lane/Lanes types, returning a merged Lanes bitmask. */ -export function mergeLanesUnsafe(a: Lanes | Lane, b: Lanes | Lane): Lanes { +export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes { return ((a as number) | (b as number)) as Lanes; } /** - * Safely performs bitwise AND on lanes. + * Performs bitwise AND on branded Lanes types, returning the intersection. */ -export function intersectLanesUnsafe(a: Lanes, b: Lanes): Lanes { +export function intersectLanes(a: Lanes, b: Lanes): Lanes { return ((a as number) & (b as number)) as Lanes; } /** - * Safely performs bitwise AND NOT on lanes. + * Performs bitwise AND NOT on branded Lanes types, removing subset from set. */ -export function removeLanesUnsafe(set: Lanes, subset: Lanes | Lane): Lanes { +export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes { return ((set as number) & ~(subset as number)) as Lanes; }