Skip to content
Merged
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
19 changes: 19 additions & 0 deletions src/node-types.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import type {
Declaration,
Block,
Block as BlockNodeAlias,
BlockChild,
SelectorList,
AtrulePrelude,
Raw,
Expand Down Expand Up @@ -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<BlockChild>()
// next_sibling is narrowed to Raw | Declaration | Atrule | Rule, not CSSNode
if (child.has_next) {
expectTypeOf(child.next_sibling).toMatchTypeOf<Raw | Declaration | Atrule | Rule>()
}
// children[] and for-of also yield BlockChild
expectTypeOf(block.children[0]).toMatchTypeOf<BlockChild>()
for (const c of block) {
expectTypeOf(c).toMatchTypeOf<BlockChild>()
}
})

it('AnyCss enables switch narrowing', () => {
// This test verifies the discriminated union works for switch narrowing.
// The function must compile without type errors.
Expand Down
18 changes: 18 additions & 0 deletions src/node-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -215,10 +215,28 @@ export type SelectorList = CSSNode &
clone(options?: CloneOptions): ToPlain<SelectorList>
}

/**
* 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<Raw | Declaration | Atrule | Rule> & {
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<BlockChild>
clone(options?: CloneOptions): ToPlain<Block>
}

Expand Down
Loading