diff --git a/src/node-types.test.ts b/src/node-types.test.ts index b836d9b..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, @@ -241,6 +242,24 @@ describe('type narrowing — compile-time', () => { } }) + 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 returns BlockChild, not the generic CSSNode + const child = block.first_child + expectTypeOf(child).toMatchTypeOf() + // next_sibling is narrowed to Raw | Declaration | Atrule | Rule, not CSSNode + 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() + } + }) + 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..75c4798 100644 --- a/src/node-types.ts +++ b/src/node-types.ts @@ -215,10 +215,28 @@ 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 }