From 8b1e09cdaffbca874b8a24f9547c3d606356d3c0 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 14:17:37 +0000 Subject: [PATCH 1/6] Narrow next_sibling type on WithChildren children Introduce ChildOf helper type so that next_sibling on any node obtained from a WithChildren parent (first_child, children[], or for-of iteration) is narrowed to T instead of the generic CSSNode. Block children now report `next_sibling: Raw | Declaration | Atrule | Rule`; SelectorList children report `next_sibling: Selector`; etc. https://claude.ai/code/session_01WGPQA9UMdtp8hd1VoBsw6h --- src/node-types.test.ts | 23 +++++++++++++++++++++++ src/node-types.ts | 30 ++++++++++++++++++++++++++---- 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/node-types.test.ts b/src/node-types.test.ts index b836d9b..d64c87a 100644 --- a/src/node-types.test.ts +++ b/src/node-types.test.ts @@ -40,6 +40,7 @@ import type { NthSelector, MediaFeature, LayerName, + Selector, } from './node-types' // --------------------------------------------------------------------------- @@ -241,6 +242,28 @@ describe('type narrowing — compile-time', () => { } }) + it('next_sibling on Block children is narrowed to Block child union', () => { + const root = parse('a { color: red; font-size: 1em }') + const rule = root.first_child! as Rule + const block = rule.block! + // first_child on Block is typed as the Block child union, not CSSNode + const child = block.first_child + expectTypeOf(child).toMatchTypeOf() + // next_sibling must also be narrowed to the same union (not CSSNode) + if (child.has_next) { + expectTypeOf(child.next_sibling).toMatchTypeOf() + } + }) + + it('next_sibling on SelectorList children is narrowed to Selector', () => { + const root = parse_selector('a, b') + const child = root.first_child + expectTypeOf(child).toMatchTypeOf() + if (child.has_next) { + expectTypeOf(child.next_sibling).toMatchTypeOf() + } + }) + it('AnyCss enables switch narrowing', () => { // This test verifies the discriminated union works for switch narrowing. // The function must compile without type errors. diff --git a/src/node-types.ts b/src/node-types.ts index 358351a..19ce9e3 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -86,6 +86,24 @@ export type CSSNode = { | { readonly has_next: true; readonly next_sibling: CSSNode } ) +/** + * Produces a version of CSSNode subtype U where next_sibling is narrowed to + * sibling union S. The conditional distributes over U (preserving each union + * member's type discriminant) while S stays fixed as the full sibling type. + */ +type _ChildOf = U extends CSSNode + ? Omit & ( + | { readonly has_next: false; readonly next_sibling: null } + | { readonly has_next: true; readonly next_sibling: S } + ) + : never + +/** + * A child of a WithChildren parent: identical to T but with next_sibling + * narrowed to T instead of the generic CSSNode. + */ +type ChildOf = _ChildOf + /** * Mixin for node types that have child nodes. * @@ -93,13 +111,17 @@ export type CSSNode = { * like StyleSheet, Block, SelectorList, Value, Function, etc. Leaf nodes * (Identifier, Number, Dimension, …) do not extend WithChildren, reflecting * that they never carry child nodes in a well-formed tree. + * + * Children are typed as ChildOf so that next_sibling on any child is + * narrowed to T (the same union as the other children of this parent) rather + * than the generic CSSNode. */ -export interface WithChildren { +export interface WithChildren { readonly has_children: boolean readonly child_count: number - readonly children: T[] - readonly first_child: T - [Symbol.iterator](): Iterator + readonly children: ChildOf[] + readonly first_child: ChildOf + [Symbol.iterator](): Iterator> } /** From fb99451a998a82a695ab4f68b196684a0b83eac2 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 14:18:04 +0000 Subject: [PATCH 2/6] Update package-lock.json after npm install https://claude.ai/code/session_01WGPQA9UMdtp8hd1VoBsw6h --- package-lock.json | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 9380375..fc4b0b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -917,16 +917,6 @@ "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@oxc-project/types": { - "version": "0.115.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", - "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", - "dev": true, - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" - } - }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz", From 775986e2e1e82eacb7c2a4be5e5707e0b05bc85e Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 8 Apr 2026 14:32:56 +0000 Subject: [PATCH 3/6] Fix TS2589 by replacing Omit with plain intersection in ChildOf Omit forces TypeScript to enumerate every key of T, which recursively expands the whole node graph (Selector -> WithChildren -> PseudoClassSelector -> WithChildren -> ...) and triggers "type instantiation is excessively deep" (TS2589). Plain intersection `U & (sibling union)` is stored lazily: TypeScript never needs to enumerate T's keys to form it, so the recursive expansion never happens. The T extends CSSNode constraint on the ChildOf wrapper (not on _ChildOf) preserves type safety at the call site without triggering the recursive check. https://claude.ai/code/session_01WGPQA9UMdtp8hd1VoBsw6h --- src/node-types.ts | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/node-types.ts b/src/node-types.ts index 19ce9e3..c3e3775 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -87,21 +87,24 @@ export type CSSNode = { ) /** - * Produces a version of CSSNode subtype U where next_sibling is narrowed to - * sibling union S. The conditional distributes over U (preserving each union - * member's type discriminant) while S stays fixed as the full sibling type. + * A child of a WithChildren parent: T intersected with a narrowed + * has_next / next_sibling discriminated union so that next_sibling is typed as + * S instead of the generic CSSNode. + * + * Uses plain intersection rather than Omit to keep instantiation shallow: + * Omit forces TypeScript to enumerate every key of U (triggering recursive + * expansion through the whole node graph), while a bare intersection is stored + * lazily and never causes TS2589. The conditional is distributive in U so + * each union member keeps its own type discriminant. */ -type _ChildOf = U extends CSSNode - ? Omit & ( +type _ChildOf = U extends unknown + ? U & ( | { readonly has_next: false; readonly next_sibling: null } | { readonly has_next: true; readonly next_sibling: S } ) : never -/** - * A child of a WithChildren parent: identical to T but with next_sibling - * narrowed to T instead of the generic CSSNode. - */ +/** A child of a WithChildren parent, with next_sibling narrowed to T. */ type ChildOf = _ChildOf /** From e08d2e379da1d797e37e701c604dd2d2b81b8e02 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 08:10:53 +0000 Subject: [PATCH 4/6] Narrow next_sibling on Block children via BlockChild type The CSS type graph has genuine cycles (Selector -> WithChildren -> PseudoClassSelector -> WithChildren -> Selector), so any generic helper placed inside WithChildren hits TypeScript's instantiation depth limit (TS2589). Instead, keep WithChildren unchanged and override first_child, children, and [Symbol.iterator] only on Block, whose child union (Raw | Declaration | Atrule | Rule) has no WithChildren of its own, making the type graph acyclic there. The new exported BlockChild type carries the narrowed next_sibling: Raw | Declaration | Atrule | Rule discriminant. https://claude.ai/code/session_01WGPQA9UMdtp8hd1VoBsw6h --- src/node-types.test.ts | 22 ++++++++----------- src/node-types.ts | 50 ++++++++++++++++++------------------------ 2 files changed, 30 insertions(+), 42 deletions(-) diff --git a/src/node-types.test.ts b/src/node-types.test.ts index d64c87a..d3bcff6 100644 --- a/src/node-types.test.ts +++ b/src/node-types.test.ts @@ -28,6 +28,7 @@ import type { Declaration, Block, Block as BlockNodeAlias, + BlockChild, SelectorList, AtrulePrelude, Raw, @@ -40,7 +41,6 @@ import type { NthSelector, MediaFeature, LayerName, - Selector, } from './node-types' // --------------------------------------------------------------------------- @@ -242,25 +242,21 @@ describe('type narrowing — compile-time', () => { } }) - it('next_sibling on Block children is narrowed to Block child union', () => { + it('Block.first_child is BlockChild with next_sibling narrowed to the Block child union', () => { const root = parse('a { color: red; font-size: 1em }') const rule = root.first_child! as Rule const block = rule.block! - // first_child on Block is typed as the Block child union, not CSSNode + // first_child on Block returns BlockChild, not the generic CSSNode const child = block.first_child - expectTypeOf(child).toMatchTypeOf() - // next_sibling must also be narrowed to the same union (not CSSNode) + expectTypeOf(child).toMatchTypeOf() + // next_sibling is narrowed to Raw | Declaration | Atrule | Rule, not CSSNode if (child.has_next) { expectTypeOf(child.next_sibling).toMatchTypeOf() } - }) - - it('next_sibling on SelectorList children is narrowed to Selector', () => { - const root = parse_selector('a, b') - const child = root.first_child - expectTypeOf(child).toMatchTypeOf() - if (child.has_next) { - expectTypeOf(child.next_sibling).toMatchTypeOf() + // children[] and for-of also yield BlockChild + expectTypeOf(block.children[0]).toMatchTypeOf() + for (const c of block) { + expectTypeOf(c).toMatchTypeOf() } }) diff --git a/src/node-types.ts b/src/node-types.ts index c3e3775..70d9617 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -86,27 +86,6 @@ export type CSSNode = { | { readonly has_next: true; readonly next_sibling: CSSNode } ) -/** - * A child of a WithChildren parent: T intersected with a narrowed - * has_next / next_sibling discriminated union so that next_sibling is typed as - * S instead of the generic CSSNode. - * - * Uses plain intersection rather than Omit to keep instantiation shallow: - * Omit forces TypeScript to enumerate every key of U (triggering recursive - * expansion through the whole node graph), while a bare intersection is stored - * lazily and never causes TS2589. The conditional is distributive in U so - * each union member keeps its own type discriminant. - */ -type _ChildOf = U extends unknown - ? U & ( - | { readonly has_next: false; readonly next_sibling: null } - | { readonly has_next: true; readonly next_sibling: S } - ) - : never - -/** A child of a WithChildren parent, with next_sibling narrowed to T. */ -type ChildOf = _ChildOf - /** * Mixin for node types that have child nodes. * @@ -114,17 +93,13 @@ type ChildOf = _ChildOf * like StyleSheet, Block, SelectorList, Value, Function, etc. Leaf nodes * (Identifier, Number, Dimension, …) do not extend WithChildren, reflecting * that they never carry child nodes in a well-formed tree. - * - * Children are typed as ChildOf so that next_sibling on any child is - * narrowed to T (the same union as the other children of this parent) rather - * than the generic CSSNode. */ -export interface WithChildren { +export interface WithChildren { readonly has_children: boolean readonly child_count: number - readonly children: ChildOf[] - readonly first_child: ChildOf - [Symbol.iterator](): Iterator> + readonly children: T[] + readonly first_child: T + [Symbol.iterator](): Iterator } /** @@ -240,10 +215,27 @@ export type SelectorList = CSSNode & clone(options?: CloneOptions): ToPlain } +/** + * A node that appears as a direct child of a Block. + * + * Identical to `Raw | Declaration | Atrule | Rule` except that `next_sibling` + * is narrowed to the same union instead of the generic `CSSNode`. This is + * safe because none of these four types use WithChildren themselves, so + * there is no recursive type graph to trigger TS2589. + */ +export type BlockChild = (Raw | Declaration | Atrule | Rule) & ( + | { readonly has_next: false; readonly next_sibling: null } + | { readonly has_next: true; readonly next_sibling: Raw | Declaration | Atrule | Rule } +) + export type Block = CSSNode & WithChildren & { readonly type: typeof BLOCK readonly is_empty: boolean + /** Block children with next_sibling narrowed to the Block child union. */ + readonly first_child: BlockChild + readonly children: BlockChild[] + [Symbol.iterator](): Iterator clone(options?: CloneOptions): ToPlain } From f6a7fbc849e37127adb23cc854b3aaabf2637f6c Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 08:16:55 +0000 Subject: [PATCH 5/6] Fix formatting (oxfmt) https://claude.ai/code/session_01WGPQA9UMdtp8hd1VoBsw6h --- src/node-types.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/node-types.ts b/src/node-types.ts index 70d9617..75c4798 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -223,10 +223,11 @@ export type SelectorList = CSSNode & * safe because none of these four types use WithChildren themselves, so * there is no recursive type graph to trigger TS2589. */ -export type BlockChild = (Raw | Declaration | Atrule | Rule) & ( - | { readonly has_next: false; readonly next_sibling: null } - | { readonly has_next: true; readonly next_sibling: Raw | Declaration | Atrule | Rule } -) +export type BlockChild = (Raw | Declaration | Atrule | Rule) & + ( + | { readonly has_next: false; readonly next_sibling: null } + | { readonly has_next: true; readonly next_sibling: Raw | Declaration | Atrule | Rule } + ) export type Block = CSSNode & WithChildren & { From f58a8f7d2cd9485d5726e406cadaf13d33ce0ee9 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 12 Apr 2026 08:21:09 +0000 Subject: [PATCH 6/6] Restore package-lock.json to main https://claude.ai/code/session_01WGPQA9UMdtp8hd1VoBsw6h --- package-lock.json | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/package-lock.json b/package-lock.json index fc4b0b4..9380375 100644 --- a/package-lock.json +++ b/package-lock.json @@ -917,6 +917,16 @@ "node": "^20.19.0 || >=22.12.0" } }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, "node_modules/@oxc-resolver/binding-android-arm-eabi": { "version": "11.19.1", "resolved": "https://registry.npmjs.org/@oxc-resolver/binding-android-arm-eabi/-/binding-android-arm-eabi-11.19.1.tgz",