Skip to content

Commit 050fdc3

Browse files
authored
Change types to base what visitor gets on tree
Previously, to define what `visitor` received could only be done through a TypeScript type parameter: ```js // This used to work but no longer!! visitParents<Heading>(tree, 'heading', (node) => { expectType<Heading>(node) }) ``` This did not look at `tree` at all (even if `tree` was hast, and as `Heading` is mdast, it would pass `Heading` to `visitor`). It also made it impossible to narrow types in JS. Given that more and more of unist and friends is now strongly typed, we can expect `tree` to be some kind of implementation of `Node` rather than the abstract `Node` interface itself. With that, we can also find all possible node types inside `tree`. This commit changes to perform the test (`'heading'`) in the type system and actually narrow down which nodes that are in `tree` match `test`. This gives us: ```js // This now works: const tree: Root = {/* … */} visitParents(tree, 'heading', (node) => { expectType<Heading>(node) }) ``` Closes GH-29.
1 parent b4624b5 commit 050fdc3

File tree

3 files changed

+153
-66
lines changed

3 files changed

+153
-66
lines changed

index.js

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,27 @@ import {visitParents, CONTINUE, SKIP, EXIT} from 'unist-util-visit-parents'
2828

2929
export {CONTINUE, SKIP, EXIT}
3030

31+
/**
32+
* Visit children of tree which pass a test
33+
*
34+
* @param tree Abstract syntax tree to walk
35+
* @param test Test, optional
36+
* @param visitor Function to run for each node
37+
* @param reverse Fisit the tree in reverse, defaults to false
38+
*/
3139
export const visit =
3240
/**
3341
* @type {(
34-
* (<T extends Node>(tree: Node, test: T['type']|Partial<T>|import('unist-util-is').TestFunctionPredicate<T>|Array.<T['type']|Partial<T>|import('unist-util-is').TestFunctionPredicate<T>>, visitor: Visitor<T>, reverse?: boolean) => void) &
35-
* ((tree: Node, test: Test, visitor: Visitor<Node>, reverse?: boolean) => void) &
36-
* ((tree: Node, visitor: Visitor<Node>, reverse?: boolean) => void)
42+
* (<Tree extends Node, Check extends Test>(tree: Tree, test: Check, visitor: Visitor<import('unist-util-visit-parents/complex-types').Matches<import('unist-util-visit-parents/complex-types').InclusiveDescendant<Tree>, Check>>, reverse?: boolean) => void) &
43+
* (<Tree extends Node>(tree: Tree, visitor: Visitor<import('unist-util-visit-parents/complex-types').InclusiveDescendant<Tree>>, reverse?: boolean) => void)
3744
* )}
3845
*/
3946
(
4047
/**
41-
* Visit children of tree which pass a test
42-
*
43-
* @param {Node} tree Abstract syntax tree to walk
44-
* @param {Test} test test Test node
45-
* @param {Visitor<Node>} visitor Function to run for each node
46-
* @param {boolean} [reverse] Fisit the tree in reverse, defaults to false
48+
* @param {Node} tree
49+
* @param {Test} test
50+
* @param {Visitor<Node>} visitor
51+
* @param {boolean} [reverse]
4752
*/
4853
function (tree, test, visitor, reverse) {
4954
if (typeof test === 'function' && typeof visitor !== 'function') {

index.test-d.ts

Lines changed: 138 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,34 @@
11
/* eslint-disable @typescript-eslint/no-confusing-void-expression, @typescript-eslint/no-empty-function */
22

3-
import {expectError} from 'tsd'
4-
import {Node, Parent} from 'unist'
3+
import {expectError, expectType} from 'tsd'
4+
import {Node, Parent, Literal} from 'unist'
5+
import {is} from 'unist-util-is'
56
import {visit, SKIP, EXIT, CONTINUE} from './index.js'
67

78
/* Setup */
8-
const sampleTree = {
9+
const sampleTree: Root = {
910
type: 'root',
1011
children: [{type: 'heading', depth: 1, children: []}]
1112
}
1213

13-
interface Heading extends Parent {
14-
type: 'heading'
15-
depth: number
16-
children: Node[]
14+
const complexTree: Root = {
15+
type: 'root',
16+
children: [
17+
{
18+
type: 'blockquote',
19+
children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}]
20+
},
21+
{
22+
type: 'paragraph',
23+
children: [
24+
{
25+
type: 'emphasis',
26+
children: [{type: 'emphasis', children: [{type: 'text', value: 'b'}]}]
27+
},
28+
{type: 'text', value: 'c'}
29+
]
30+
}
31+
]
1732
}
1833

1934
interface Element extends Parent {
@@ -24,6 +39,43 @@ interface Element extends Parent {
2439
children: Node[]
2540
}
2641

42+
type Content = Flow | Phrasing
43+
44+
interface Root extends Parent {
45+
type: 'root'
46+
children: Flow[]
47+
}
48+
49+
type Flow = Blockquote | Heading | Paragraph
50+
51+
interface Blockquote extends Parent {
52+
type: 'blockquote'
53+
children: Flow[]
54+
}
55+
56+
interface Heading extends Parent {
57+
type: 'heading'
58+
depth: number
59+
children: Phrasing[]
60+
}
61+
62+
interface Paragraph extends Parent {
63+
type: 'paragraph'
64+
children: Phrasing[]
65+
}
66+
67+
type Phrasing = Text | Emphasis
68+
69+
interface Emphasis extends Parent {
70+
type: 'emphasis'
71+
children: Phrasing[]
72+
}
73+
74+
interface Text extends Literal {
75+
type: 'text'
76+
value: string
77+
}
78+
2779
const isNode = (node: unknown): node is Node =>
2880
typeof node === 'object' && node !== null && 'type' in node
2981
const headingTest = (node: unknown): node is Heading =>
@@ -36,68 +88,98 @@ expectError(visit())
3688
expectError(visit(sampleTree))
3789

3890
/* Visit without test. */
39-
visit(sampleTree, (_) => {})
40-
visit(sampleTree, (_: Node) => {})
41-
expectError(visit(sampleTree, (_: Element) => {}))
42-
expectError(visit(sampleTree, (_: Heading) => {}))
91+
visit(sampleTree, (node) => {
92+
expectType<Root | Content>(node)
93+
})
4394

4495
/* Visit with type test. */
45-
visit(sampleTree, 'heading', (_) => {})
46-
visit(sampleTree, 'heading', (_: Heading) => {})
47-
expectError(visit(sampleTree, 'not-a-heading', (_: Heading) => {}))
48-
expectError(visit(sampleTree, 'element', (_: Heading) => {}))
49-
50-
visit(sampleTree, 'element', (_) => {})
51-
visit(sampleTree, 'element', (_: Element) => {})
52-
expectError(visit(sampleTree, 'not-an-element', (_: Element) => {}))
96+
visit(sampleTree, 'heading', (node) => {
97+
expectType<Heading>(node)
98+
})
99+
visit(sampleTree, 'element', (node) => {
100+
// Not in tree.
101+
expectType<never>(node)
102+
})
53103
expectError(visit(sampleTree, 'heading', (_: Element) => {}))
54104

55105
/* Visit with object test. */
56-
visit(sampleTree, {type: 'heading'}, (_) => {})
57-
visit(sampleTree, {random: 'property'}, (_) => {})
58-
59-
visit(sampleTree, {type: 'heading'}, (_: Heading) => {})
60-
visit(sampleTree, {type: 'heading', depth: 2}, (_: Heading) => {})
61-
expectError(visit(sampleTree, {type: 'element'}, (_: Heading) => {}))
62-
expectError(
63-
visit(sampleTree, {type: 'heading', depth: '2'}, (_: Heading) => {})
64-
)
65-
66-
visit(sampleTree, {type: 'element'}, (_: Element) => {})
67-
visit(sampleTree, {type: 'element', tagName: 'section'}, (_: Element) => {})
68-
69-
expectError(visit(sampleTree, {type: 'heading'}, (_: Element) => {}))
70-
71-
expectError(
72-
visit(sampleTree, {type: 'element', tagName: true}, (_: Element) => {})
73-
)
106+
visit(sampleTree, {depth: 1}, (node) => {
107+
expectType<Heading>(node)
108+
})
109+
visit(sampleTree, {random: 'property'}, (node) => {
110+
expectType<never>(node)
111+
})
112+
visit(sampleTree, {type: 'heading', depth: '2'}, (node) => {
113+
// Not in tree.
114+
expectType<never>(node)
115+
})
116+
visit(sampleTree, {tagName: 'section'}, (node) => {
117+
// Not in tree.
118+
expectType<never>(node)
119+
})
120+
visit(sampleTree, {type: 'element', tagName: 'section'}, (node) => {
121+
// Not in tree.
122+
expectType<never>(node)
123+
})
74124

75125
/* Visit with function test. */
76-
visit(sampleTree, headingTest, (_) => {})
77-
visit(sampleTree, headingTest, (_: Heading) => {})
126+
visit(sampleTree, headingTest, (node) => {
127+
expectType<Heading>(node)
128+
})
78129
expectError(visit(sampleTree, headingTest, (_: Element) => {}))
79-
80-
visit(sampleTree, elementTest, (_) => {})
81-
visit(sampleTree, elementTest, (_: Element) => {})
82-
expectError(visit(sampleTree, elementTest, (_: Heading) => {}))
130+
visit(sampleTree, elementTest, (node) => {
131+
// Not in tree.
132+
expectType<never>(node)
133+
})
83134

84135
/* Visit with array of tests. */
85-
visit(sampleTree, ['ParagraphNode', {type: 'element'}, headingTest], (_) => {})
136+
visit(sampleTree, ['heading', {depth: 1}, headingTest], (node) => {
137+
// Unfortunately TS casts things in arrays too vague.
138+
expectType<Root | Content>(node)
139+
})
86140

87141
/* Visit returns action. */
88-
visit(sampleTree, 'heading', (_) => CONTINUE)
89-
visit(sampleTree, 'heading', (_) => EXIT)
90-
visit(sampleTree, 'heading', (_) => SKIP)
91-
expectError(visit(sampleTree, 'heading', (_) => 'random'))
142+
visit(sampleTree, () => CONTINUE)
143+
visit(sampleTree, () => EXIT)
144+
visit(sampleTree, () => SKIP)
145+
expectError(visit(sampleTree, () => 'random'))
92146

93147
/* Visit returns index. */
94-
visit(sampleTree, 'heading', (_) => 0)
95-
visit(sampleTree, 'heading', (_) => 1)
148+
visit(sampleTree, () => 0)
149+
visit(sampleTree, () => 1)
96150

97151
/* Visit returns tuple. */
98-
visit(sampleTree, 'heading', (_) => [CONTINUE, 1])
99-
visit(sampleTree, 'heading', (_) => [EXIT, 1])
100-
visit(sampleTree, 'heading', (_) => [SKIP, 1])
101-
visit(sampleTree, 'heading', (_) => [SKIP])
102-
expectError(visit(sampleTree, 'heading', (_) => [1]))
103-
expectError(visit(sampleTree, 'heading', (_) => ['random', 1]))
152+
visit(sampleTree, () => [CONTINUE, 1])
153+
visit(sampleTree, () => [EXIT, 1])
154+
visit(sampleTree, () => [SKIP, 1])
155+
visit(sampleTree, () => [SKIP])
156+
expectError(visit(sampleTree, () => [1]))
157+
expectError(visit(sampleTree, () => ['random', 1]))
158+
159+
/* Should infer children from the given tree. */
160+
visit(complexTree, (node) => {
161+
expectType<Root | Content>(node)
162+
})
163+
164+
const blockquote = complexTree.children[0]
165+
if (is<Blockquote>(blockquote, 'blockquote')) {
166+
visit(blockquote, (node) => {
167+
expectType<Content>(node)
168+
})
169+
}
170+
171+
const paragraph = complexTree.children[1]
172+
if (is<Paragraph>(paragraph, 'paragraph')) {
173+
visit(paragraph, (node) => {
174+
expectType<Paragraph | Phrasing>(node)
175+
})
176+
177+
const child = paragraph.children[1]
178+
179+
if (is<Emphasis>(child, 'emphasis')) {
180+
visit(child, 'blockquote', (node) => {
181+
// `blockquote` does not exist in phrasing.
182+
expectType<never>(node)
183+
})
184+
}
185+
}

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
"dependencies": {
5050
"@types/unist": "^2.0.0",
5151
"unist-util-is": "^5.0.0",
52-
"unist-util-visit-parents": "^4.0.0"
52+
"unist-util-visit-parents": "^5.0.0"
5353
},
5454
"devDependencies": {
5555
"@types/tape": "^4.0.0",

0 commit comments

Comments
 (0)