Skip to content

Commit 1fc398b

Browse files
committedApr 25, 2021
Add JSDoc based types
·
3.0.02.0.0
1 parent 33d1f77 commit 1fc398b

File tree

6 files changed

+371
-64
lines changed

6 files changed

+371
-64
lines changed
 

‎.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
.DS_Store
2+
*.d.ts
23
*.log
34
coverage/
45
node_modules/

‎index.js

Lines changed: 185 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,70 +1,167 @@
1-
// Check if if `node` is an `element` and whether it passes the given test.
2-
// eslint-disable-next-line max-params
3-
export function isElement(node, test, index, parent, context) {
4-
var check = convertElement(test)
5-
6-
if (
7-
index !== undefined &&
8-
index !== null &&
9-
(typeof index !== 'number' ||
10-
index < 0 ||
11-
index === Number.POSITIVE_INFINITY)
12-
) {
13-
throw new Error('Expected positive finite index for child node')
14-
}
1+
/**
2+
* @typedef {import('unist').Node} Node
3+
* @typedef {import('unist').Parent} Parent
4+
* @typedef {import('hast').Element} Element
5+
*
6+
* @typedef {string} TagName
7+
*/
8+
9+
/**
10+
* Check if an element passes a test
11+
*
12+
* @callback TestFunctionAnything
13+
* @param {Element} element
14+
* @param {number} [index]
15+
* @param {Parent} [parent]
16+
* @returns {boolean|void}
17+
*/
18+
19+
/**
20+
* Check if an element passes a certain node test
21+
*
22+
* @template {Element} X
23+
* @callback TestFunctionPredicate
24+
* @param {X} element
25+
* @param {number} [index]
26+
* @param {Parent} [parent]
27+
* @returns {element is X}
28+
*/
29+
30+
/**
31+
* Check if a node is an element and passes a certain node test
32+
*
33+
* @template {Element} Y
34+
* @callback AssertPredicate
35+
* @param {unknown} [node]
36+
* @param {number} [index]
37+
* @param {Parent} [parent]
38+
* @returns {node is Y}
39+
*/
40+
41+
// Check if `node` is an `element` and whether it passes the given test.
42+
export const isElement =
43+
/**
44+
* Check if a node is an element and passes a test.
45+
* When a `parent` node is known the `index` of node should also be given.
46+
*
47+
* @type {(
48+
* (<T extends Element>(node: unknown, test: T['tagName']|TestFunctionPredicate<T>|Array.<T['tagName']|TestFunctionPredicate<T>>, index?: number, parent?: Parent, context?: unknown) => node is T) &
49+
* ((node?: unknown, test?: null|undefined|TagName|TestFunctionAnything|Array.<TagName|TestFunctionAnything>, index?: number, parent?: Parent, context?: unknown) => node is Element)
50+
* )}
51+
*/
52+
(
53+
/**
54+
* Check if a node passes a test.
55+
* When a `parent` node is known the `index` of node should also be given.
56+
*
57+
* @param {unknown} [node] Node to check
58+
* @param {null|undefined|TagName|TestFunctionAnything|Array.<TagName|TestFunctionAnything>} [test]
59+
* When nullish, checks if `node` is a `Node`.
60+
* When `string`, works like passing `function (node) {return node.type === test}`.
61+
* When `function` checks if function passed the node is true.
62+
* When `array`, checks any one of the subtests pass.
63+
* @param {number} [index] Position of `node` in `parent`
64+
* @param {Parent} [parent] Parent of `node`
65+
* @param {unknown} [context] Context object to invoke `test` with
66+
* @returns {boolean} Whether test passed and `node` is an `Element` (object with `type` set to `element` and `tagName` set to a non-empty string).
67+
*/
68+
// eslint-disable-next-line max-params
69+
function (node, test, index, parent, context) {
70+
var check = convertElement(test)
71+
72+
if (
73+
index !== undefined &&
74+
index !== null &&
75+
(typeof index !== 'number' ||
76+
index < 0 ||
77+
index === Number.POSITIVE_INFINITY)
78+
) {
79+
throw new Error('Expected positive finite index for child node')
80+
}
1581

16-
if (
17-
parent !== undefined &&
18-
parent !== null &&
19-
(!parent.type || !parent.children)
20-
) {
21-
throw new Error('Expected parent node')
22-
}
82+
if (
83+
parent !== undefined &&
84+
parent !== null &&
85+
(!parent.type || !parent.children)
86+
) {
87+
throw new Error('Expected parent node')
88+
}
2389

24-
if (!node || !node.type || typeof node.type !== 'string') {
25-
return false
26-
}
90+
// @ts-ignore Looks like a node.
91+
if (!node || !node.type || typeof node.type !== 'string') {
92+
return false
93+
}
2794

28-
if (
29-
(parent === undefined || parent === null) !==
30-
(index === undefined || index === null)
31-
) {
32-
throw new Error('Expected both parent and index')
33-
}
95+
if (
96+
(parent === undefined || parent === null) !==
97+
(index === undefined || index === null)
98+
) {
99+
throw new Error('Expected both parent and index')
100+
}
34101

35-
return check.call(context, node, index, parent)
36-
}
102+
return check.call(context, node, index, parent)
103+
}
104+
)
37105

38-
export function convertElement(test) {
39-
if (test === undefined || test === null) {
40-
return element
41-
}
106+
export const convertElement =
107+
/**
108+
* @type {(
109+
* (<T extends Element>(test: T['tagName']|TestFunctionPredicate<T>) => AssertPredicate<T>) &
110+
* ((test?: null|undefined|TagName|TestFunctionAnything|Array.<TagName|TestFunctionAnything>) => AssertPredicate<Element>)
111+
* )}
112+
*/
113+
(
114+
/**
115+
* Generate an assertion from a check.
116+
* @param {null|undefined|TagName|TestFunctionAnything|Array.<TagName|TestFunctionAnything>} [test]
117+
* When nullish, checks if `node` is a `Node`.
118+
* When `string`, works like passing `function (node) {return node.type === test}`.
119+
* When `function` checks if function passed the node is true.
120+
* When `object`, checks that all keys in test are in node, and that they have (strictly) equal values.
121+
* When `array`, checks any one of the subtests pass.
122+
* @returns {AssertPredicate<Element>}
123+
*/
124+
function (test) {
125+
if (test === undefined || test === null) {
126+
return element
127+
}
42128

43-
if (typeof test === 'string') {
44-
return tagNameFactory(test)
45-
}
129+
if (typeof test === 'string') {
130+
return tagNameFactory(test)
131+
}
46132

47-
if (typeof test === 'object') {
48-
return anyFactory(test)
49-
}
133+
if (typeof test === 'object') {
134+
return anyFactory(test)
135+
}
50136

51-
if (typeof test === 'function') {
52-
return callFactory(test)
53-
}
137+
if (typeof test === 'function') {
138+
return castFactory(test)
139+
}
54140

55-
throw new Error('Expected function, string, or array as test')
56-
}
141+
throw new Error('Expected function, string, or array as test')
142+
}
143+
)
57144

145+
/**
146+
* @param {Array.<TagName|TestFunctionAnything>} tests
147+
* @returns {AssertPredicate<Element>}
148+
*/
58149
function anyFactory(tests) {
59-
var index = -1
150+
/** @type {Array.<AssertPredicate<Element>>} */
60151
var checks = []
152+
var index = -1
61153

62154
while (++index < tests.length) {
63155
checks[index] = convertElement(tests[index])
64156
}
65157

66-
return any
158+
return castFactory(any)
67159

160+
/**
161+
* @this {unknown}
162+
* @param {unknown[]} parameters
163+
* @returns {node is Element}
164+
*/
68165
function any(...parameters) {
69166
var index = -1
70167

@@ -78,30 +175,55 @@ function anyFactory(tests) {
78175
}
79176
}
80177

81-
// Utility to convert a string a tag name check.
82-
function tagNameFactory(test) {
178+
/**
179+
* Utility to convert a string into a function which checks a given node’s tag
180+
* name for said string.
181+
*
182+
* @param {TagName} check
183+
* @returns {AssertPredicate<Element>}
184+
*/
185+
function tagNameFactory(check) {
83186
return tagName
84187

188+
/**
189+
* @param {Node} node
190+
* @returns {node is Element}
191+
*/
85192
function tagName(node) {
86-
return element(node) && node.tagName === test
193+
return element(node) && node.tagName === check
87194
}
88195
}
89196

90-
// Utility to convert a function check.
91-
function callFactory(test) {
92-
return call
93-
94-
function call(node, ...parameters) {
95-
return element(node) && Boolean(test.call(this, node, ...parameters))
197+
/**
198+
* @param {TestFunctionAnything} check
199+
* @returns {AssertPredicate<Element>}
200+
*/
201+
function castFactory(check) {
202+
return assertion
203+
204+
/**
205+
* @this {unknown}
206+
* @param {Node} node
207+
* @param {Array.<unknown>} parameters
208+
* @returns {node is Element}
209+
*/
210+
function assertion(node, ...parameters) {
211+
return element(node) && Boolean(check.call(this, node, ...parameters))
96212
}
97213
}
98214

99-
// Utility to return true if this is an element.
215+
/**
216+
* Utility to return true if this is an element.
217+
* @param {unknown} node
218+
* @returns {node is Element}
219+
*/
100220
function element(node) {
101-
return (
221+
return Boolean(
102222
node &&
103-
typeof node === 'object' &&
104-
node.type === 'element' &&
105-
typeof node.tagName === 'string'
223+
typeof node === 'object' &&
224+
// @ts-ignore Looks like a node.
225+
node.type === 'element' &&
226+
// @ts-ignore Looks like an element.
227+
typeof node.tagName === 'string'
106228
)
107229
}

‎index.test-d.ts

Lines changed: 135 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
import unified from 'unified'
2+
import {Node} from 'unist'
3+
import {expectType, expectNotType, expectError} from 'tsd'
4+
import {Element} from 'hast'
5+
import {isElement, convertElement} from './index.js'
6+
7+
/* Setup. */
8+
interface Section extends Element {
9+
tagName: 'section'
10+
}
11+
12+
interface Article extends Element {
13+
tagName: 'article'
14+
}
15+
16+
const section: Element = {
17+
type: 'element',
18+
tagName: 'section',
19+
properties: {},
20+
children: [{type: 'text', value: 'x'}]
21+
}
22+
23+
const article: Element = {
24+
type: 'element',
25+
tagName: 'article',
26+
properties: {},
27+
children: [{type: 'text', value: 'x'}]
28+
}
29+
30+
const isSection = (element: Element): element is Section =>
31+
element.tagName === 'section'
32+
33+
isElement()
34+
35+
/* Missing parameters. */
36+
expectError(isElement<Section>())
37+
38+
/* Types cannot be narrowed without predicate. */
39+
expectType<boolean>(isElement(section))
40+
41+
/* Incorrect generic. */
42+
expectError(isElement<string>(section, 'section'))
43+
expectError(isElement<boolean>(section, 'section'))
44+
expectError(isElement<Record<string, unknown>>(section, 'section'))
45+
46+
/* Should be assignable to boolean. */
47+
expectType<boolean>(isElement<Section>(section, 'section'))
48+
49+
/* Test isElement optional */
50+
expectType<boolean>(isElement(section))
51+
expectType<boolean>(isElement(section, null))
52+
expectType<boolean>(isElement(section, undefined))
53+
/* But not with a type predicate */
54+
expectError(isElement<Node>(section)) // But not with a type predicate
55+
expectError(isElement<Node>(section, null))
56+
expectError(isElement<Node>(section, undefined))
57+
58+
/* Should support string tests. */
59+
expectType<boolean>(isElement<Section>(section, 'section'))
60+
expectType<boolean>(isElement<Section>(article, 'section'))
61+
expectError(isElement<Section>(section, 'article'))
62+
63+
if (isElement<Section>(section, 'section')) {
64+
expectType<Section>(section)
65+
expectNotType<Article>(section)
66+
}
67+
68+
expectType<boolean>(isElement<Article>(article, 'article'))
69+
expectType<boolean>(isElement<Article>(section, 'article'))
70+
expectError(isElement<Article>(article, 'section'))
71+
72+
if (isElement<Article>(article, 'article')) {
73+
expectType<Article>(article)
74+
expectNotType<Section>(article)
75+
}
76+
77+
/* Should support function tests. */
78+
expectType<boolean>(isElement(section, isSection))
79+
expectType<boolean>(isElement(article, isSection))
80+
81+
if (isElement(section, isSection)) {
82+
expectType<Section>(section)
83+
expectNotType<Article>(section)
84+
}
85+
86+
expectType<boolean>(isElement(article, isSection))
87+
expectType<boolean>(isElement(section, isSection))
88+
expectError(isElement<Article>(article, isSection))
89+
90+
if (isElement(section, isSection)) {
91+
expectType<Section>(section)
92+
}
93+
94+
/* Should support array tests. */
95+
expectType<boolean>(
96+
isElement<Article | Section>(section, ['article', isSection])
97+
)
98+
99+
if (isElement<Article | Section>(section, ['article', isSection])) {
100+
switch (section.tagName) {
101+
case 'section': {
102+
expectType<Section>(section)
103+
break
104+
}
105+
106+
case 'article': {
107+
expectType<Article>(section)
108+
break
109+
}
110+
111+
default: {
112+
break
113+
}
114+
}
115+
}
116+
117+
/* Should support being used in a unified transform. */
118+
unified().use(() => (tree) => {
119+
if (isElement<Section>(tree, 'section')) {
120+
expectType<Section>(tree)
121+
// Do something
122+
}
123+
124+
return tree
125+
})
126+
127+
/* Should support `convert`. */
128+
convertElement<Section>('section')
129+
expectError(convertElement<Section>('article'))
130+
convertElement<Section>(isSection)
131+
expectError(convertElement<Article>(isSection))
132+
convertElement()
133+
convertElement(null)
134+
convertElement(undefined)
135+
expectError(convertElement<Article>())

‎package.json

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,36 @@
2626
"sideEffects": false,
2727
"type": "module",
2828
"main": "index.js",
29+
"types": "index.d.ts",
2930
"files": [
31+
"index.d.ts",
3032
"index.js"
3133
],
34+
"dependencies": {
35+
"@types/hast": "^2.0.0",
36+
"@types/unist": "^2.0.0"
37+
},
3238
"devDependencies": {
39+
"@types/tape": "^4.0.0",
3340
"c8": "^7.0.0",
3441
"prettier": "^2.0.0",
3542
"remark-cli": "^9.0.0",
3643
"remark-preset-wooorm": "^8.0.0",
44+
"rimraf": "^3.0.0",
3745
"tape": "^5.0.0",
46+
"tsd": "^0.14.0",
47+
"type-coverage": "^2.0.0",
48+
"typescript": "^4.0.0",
49+
"unified": "^9.0.0",
3850
"xo": "^0.38.0"
3951
},
4052
"scripts": {
53+
"prepack": "npm run build && npm run format",
54+
"build": "rimraf \"*.d.ts\" && tsc && tsd && type-coverage",
4155
"format": "remark . -qfo && prettier . -w --loglevel warn && xo --fix",
4256
"test-api": "node test.js",
4357
"test-coverage": "c8 --check-coverage --branches 100 --functions 100 --lines 100 --statements 100 --reporter lcov node test.js",
44-
"test": "npm run format && npm run test-coverage"
58+
"test": "npm run build && npm run format && npm run test-coverage"
4559
},
4660
"prettier": {
4761
"tabWidth": 2,
@@ -62,5 +76,10 @@
6276
"plugins": [
6377
"preset-wooorm"
6478
]
79+
},
80+
"typeCoverage": {
81+
"atLeast": 100,
82+
"detail": true,
83+
"strict": true
6584
}
6685
}

‎test.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,8 @@
1+
/**
2+
* @typedef {import('unist').Parent} Parent
3+
* @typedef {import('hast').Element} Element
4+
*/
5+
16
import test from 'tape'
27
import {isElement} from './index.js'
38

@@ -7,6 +12,7 @@ test('isElement', function (t) {
712

813
t.throws(
914
function () {
15+
// @ts-ignore runtime.
1016
isElement(null, true)
1117
},
1218
/Expected function, string, or array as test/,
@@ -134,6 +140,12 @@ test('isElement', function (t) {
134140
st.equal(
135141
isElement(
136142
root.children[0],
143+
/**
144+
* @this {ctx}
145+
* @param {Element} a
146+
* @param {number} b
147+
* @param {Parent} c
148+
*/
137149
function (node, index, parent) {
138150
st.equal(node, root.children[0], 'should pass `node` to test')
139151
st.equal(index, 0, 'should pass `index` to test')
@@ -166,6 +178,7 @@ test('isElement', function (t) {
166178

167179
st.throws(
168180
function () {
181+
// @ts-ignore runtime.
169182
isElement(root.children[0], function () {}, false)
170183
},
171184
/Expected positive finite index for child node/,
@@ -190,6 +203,7 @@ test('isElement', function (t) {
190203

191204
st.throws(
192205
function () {
206+
// @ts-ignore runtime.
193207
isElement(root.children[0], function () {}, 0, true)
194208
},
195209
/Expected parent node/,
@@ -198,6 +212,7 @@ test('isElement', function (t) {
198212

199213
st.throws(
200214
function () {
215+
// @ts-ignore runtime.
201216
isElement(root.children[0], function () {}, 0, {type: 'root'})
202217
},
203218
/Expected parent node/,

‎tsconfig.json

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"include": ["*.js"],
3+
"compilerOptions": {
4+
"target": "ES2020",
5+
"lib": ["ES2020"],
6+
"module": "ES2020",
7+
"moduleResolution": "node",
8+
"allowJs": true,
9+
"checkJs": true,
10+
"declaration": true,
11+
"emitDeclarationOnly": true,
12+
"allowSyntheticDefaultImports": true,
13+
"skipLibCheck": true
14+
}
15+
}

0 commit comments

Comments
 (0)
Please sign in to comment.