Skip to content

Commit 66e15b8

Browse files
committed
Change to improve type inferral
1 parent 402d98b commit 66e15b8

File tree

5 files changed

+120
-130
lines changed

5 files changed

+120
-130
lines changed

lib/complex-types.d.ts

Lines changed: 0 additions & 23 deletions
This file was deleted.

lib/index.js

Lines changed: 79 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,44 +1,102 @@
11
/**
2-
* @typedef {import('unist').Node} Node
2+
* @typedef {import('unist').Node} UnistNode
3+
* @typedef {import('unist').Parent} UnistParent
34
*/
45

56
/**
6-
* @template {Node} [Kind=Node]
7+
* @typedef {0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10} Uint
8+
* Number; capped reasonably.
9+
* @see https://github.com/syntax-tree/unist-util-visit-parents/blob/main/lib/index.js
10+
*/
11+
12+
/**
13+
* @typedef {I extends 0 ? 1 : I extends 1 ? 2 : I extends 2 ? 3 : I extends 3 ? 4 : I extends 4 ? 5 : I extends 5 ? 6 : I extends 6 ? 7 : I extends 7 ? 8 : I extends 8 ? 9 : 10} Increment
14+
* Increment a number in the type system.
15+
* @template {Uint} [I=0]
16+
* Index.
17+
* @see https://github.com/syntax-tree/unist-util-visit-parents/blob/main/lib/index.js
18+
*/
19+
20+
/**
21+
* @typedef {(
22+
* Tree extends UnistParent
23+
* ? Depth extends Max
24+
* ? Tree
25+
* : Tree | InclusiveDescendant<Tree['children'][number], Max, Increment<Depth>>
26+
* : Tree
27+
* )} InclusiveDescendant
28+
* Collect all (inclusive) descendants of `Tree`.
29+
*
30+
* > 👉 **Note**: for performance reasons, this seems to be the fastest way to
31+
* > recurse without actually running into an infinite loop, which the
32+
* > previous version did.
33+
* >
34+
* > Practically, a max of `2` is typically enough assuming a `Root` is
35+
* > passed, but it doesn’t improve performance.
36+
* > It gets higher with `List > ListItem > Table > TableRow > TableCell`.
37+
* > Using up to `10` doesn’t hurt or help either.
38+
* @template {UnistNode} Tree
39+
* Tree type.
40+
* @template {Uint} [Max=10]
41+
* Max; searches up to this depth.
42+
* @template {Uint} [Depth=0]
43+
* Current depth.
44+
* @see https://github.com/syntax-tree/unist-util-visit-parents/blob/main/lib/index.js
45+
*/
46+
47+
/**
48+
* @template {UnistNode} Tree
749
* Node type.
8-
* @typedef {import('./complex-types.js').MapFunction<Kind>} MapFunction
50+
* @typedef {(
51+
* (
52+
* node: Readonly<InclusiveDescendant<Tree>>,
53+
* index: number | undefined,
54+
* parent: Readonly<Extract<InclusiveDescendant<Tree>, UnistParent>> | undefined
55+
* ) => Tree | InclusiveDescendant<Tree>
56+
* )} MapFunction
957
* Function called with a node, its index, and its parent to produce a new
1058
* node.
1159
*/
1260

1361
/**
1462
* Create a new tree by mapping all nodes with the given function.
1563
*
16-
* @template {Node} Kind
64+
* @template {UnistNode} Tree
1765
* Type of input tree.
18-
* @param {Kind} tree
66+
* @param {Tree} tree
1967
* Tree to map.
20-
* @param {MapFunction<Kind>} mapFunction
68+
* @param {MapFunction<Tree>} mapFunction
2169
* Function called with a node, its index, and its parent to produce a new
2270
* node.
23-
* @returns {Kind}
71+
* @returns {InclusiveDescendant<Tree>}
2472
* New mapped tree.
2573
*/
2674
export function map(tree, mapFunction) {
27-
// @ts-expect-error Looks like a children.
28-
return preorder(tree, null, null)
29-
30-
/** @type {import('./complex-types.js').MapFunction<Kind>} */
31-
function preorder(node, index, parent) {
32-
const newNode = Object.assign({}, mapFunction(node, index, parent))
33-
34-
if ('children' in node) {
35-
// @ts-expect-error Looks like a parent.
36-
newNode.children = node.children.map(function (
37-
/** @type {import('./complex-types.js').InclusiveDescendant<Kind>} */ child,
38-
/** @type {number} */ index
39-
) {
40-
return preorder(child, index, node)
75+
const result = preorder(tree, undefined, undefined)
76+
77+
// @ts-expect-error: the new node is expected.
78+
return result
79+
80+
/** @type {MapFunction<UnistNode | UnistParent>} */
81+
function preorder(oldNode, index, parent) {
82+
/** @type {UnistNode} */
83+
const newNode = {
84+
...mapFunction(
85+
// @ts-expect-error: the old node is expected.
86+
oldNode,
87+
index,
88+
parent
89+
)
90+
}
91+
92+
if ('children' in oldNode) {
93+
const newNodeAstParent = /** @type {UnistParent} */ (newNode)
94+
95+
const nextChildren = oldNode.children.map(function (child, index) {
96+
return preorder(child, index, oldNode)
4197
})
98+
99+
newNodeAstParent.children = nextChildren
42100
}
43101

44102
return newNode

readme.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,13 +80,13 @@ const tree = u('tree', [
8080
u('leaf', 'leaf 3')
8181
])
8282

83-
const next = map(tree, (node) => {
83+
const next = map(tree, function (node) {
8484
return node.type === 'leaf'
8585
? Object.assign({}, node, {value: 'CHANGED'})
8686
: node
8787
})
8888

89-
console.dir(next, {depth: null})
89+
console.dir(next, {depth: undefined})
9090
```
9191

9292
Yields:

test.js

Lines changed: 38 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -1,97 +1,52 @@
11
/**
2-
* @typedef {{type: 'leaf', value: string}} Leaf
3-
* @typedef {{type: 'node', children: Array<Node | Leaf>}} Node
4-
* @typedef {{type: 'root', children: Array<Node | Leaf>}} Root
5-
* @typedef {Root | Node | Leaf} AnyNode
2+
* @typedef {import('mdast').Content} Content
3+
* @typedef {import('mdast').Root} Root
64
*/
75

86
import assert from 'node:assert/strict'
97
import test from 'node:test'
108
import {u} from 'unist-builder'
119
import {map} from './index.js'
12-
import * as mod from './index.js'
1310

14-
test('map', function () {
15-
assert.deepEqual(
16-
Object.keys(mod).sort(),
17-
['map'],
18-
'should expose the public api'
19-
)
20-
21-
/** @type {Root} */
22-
const rootA = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')])
23-
assert.deepEqual(
24-
map(rootA, changeLeaf),
25-
u('root', [u('node', [u('leaf', 'CHANGED')]), u('leaf', 'CHANGED')]),
26-
'should map the specified node'
27-
)
28-
29-
/** @type {Root} */
30-
const rootB = {
31-
type: 'root',
32-
children: [
33-
{type: 'node', children: [{type: 'leaf', value: '1'}]},
34-
{type: 'leaf', value: '2'}
35-
]
36-
}
37-
assert.deepEqual(
38-
map(rootB, changeLeaf),
39-
u('root', [u('node', [u('leaf', 'CHANGED')]), u('leaf', 'CHANGED')]),
40-
'should map the specified node'
41-
)
42-
43-
/** @type {Root} */
44-
const rootC = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')])
45-
assert.deepEqual(
46-
// @ts-expect-error: invalid:
47-
map(rootC, nullLeaf),
48-
// @ts-expect-error: not valid but tested anyway.
49-
u('root', [u('node', [{}]), {}]),
50-
'should work when retuning an empty object'
51-
)
52-
53-
assert.deepEqual(
54-
// @ts-expect-error runtime.
55-
map({}, addValue),
56-
{value: 'test'},
57-
'should work when passing an empty object'
58-
)
59-
60-
/** @type {Root} */
61-
const tree = u('root', [u('node', [u('leaf', '1')]), u('leaf', '2')])
62-
63-
assert.deepEqual(
64-
map(tree, asIs),
65-
tree,
66-
'should support an explicitly typed `MapFunction`'
67-
)
11+
test('map', async function (t) {
12+
await t.test('should expose the public api', async function () {
13+
assert.deepEqual(Object.keys(await import('./index.js')).sort(), ['map'])
14+
})
15+
16+
await t.test('should map the specified node', async function () {
17+
/** @type {Root} */
18+
const tree = u('root', [u('paragraph', [u('text', '1')]), u('text', '2')])
19+
20+
assert.deepEqual(
21+
map(tree, changeLeaf),
22+
u('root', [u('paragraph', [u('text', 'CHANGED')]), u('text', 'CHANGED')])
23+
)
24+
})
25+
26+
await t.test('should map the specified node', async function () {
27+
/** @type {Root} */
28+
const tree = u('root', [u('paragraph', [u('text', '1')]), u('text', '2')])
29+
30+
assert.deepEqual(
31+
map(tree, changeLeaf),
32+
u('root', [u('paragraph', [u('text', 'CHANGED')]), u('text', 'CHANGED')])
33+
)
34+
})
35+
36+
await t.test('should work when passing an empty object', async function () {
37+
assert.deepEqual(
38+
// @ts-expect-error: check how not-a-node is handled.
39+
map({}, function () {
40+
return {value: 'test'}
41+
}),
42+
{value: 'test'}
43+
)
44+
})
6845
})
6946

70-
/**
71-
* @param {AnyNode} node
72-
* @returns {AnyNode}
73-
*/
74-
function changeLeaf(node) {
75-
return node.type === 'leaf'
76-
? Object.assign({}, node, {value: 'CHANGED'})
77-
: node
78-
}
79-
80-
/**
81-
* @param {AnyNode} node
82-
* @returns {Root | Node | null}
83-
*/
84-
function nullLeaf(node) {
85-
return node.type === 'leaf' ? null : node
86-
}
87-
88-
function addValue() {
89-
return {value: 'test'}
90-
}
91-
9247
/**
9348
* @type {import('./index.js').MapFunction<Root>}
9449
*/
95-
function asIs(node) {
96-
return node
50+
function changeLeaf(node) {
51+
return node.type === 'text' ? {...node, value: 'CHANGED'} : node
9752
}

tsconfig.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,5 @@
1111
"target": "es2020"
1212
},
1313
"exclude": ["coverage/", "node_modules/"],
14-
"include": ["**/*.js", "lib/complex-types.d.ts"]
14+
"include": ["**/*.js"]
1515
}

0 commit comments

Comments
 (0)