Skip to content

Commit 0a5de58

Browse files
committed
Change types to work w/o explicit type parameter
Previously, the type parameter of the kind of node that was checked for needed to be passed. This node kind had to be matched by the given runtime `test`. At that point, it was known that node would be of that kind. Now, the given runtime test is used in the type system to narrow the given `node` value down. This means that the types can become much simpler. Instead of `AssertAnything` and `AssertPredicate`, use the new `Check`. Instead of `TestFunctionAnything` and `TestFunctionPredicate`, use `TestFunction`. Instead of `PredicateTest`, use `Test`.
1 parent b4e28f5 commit 0a5de58

File tree

6 files changed

+381
-450
lines changed

6 files changed

+381
-450
lines changed

index.js

+2-17
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,7 @@
11
/**
2-
* @typedef {import('./lib/index.js').AssertAnything} AssertAnything
2+
* @typedef {import('./lib/index.js').Check} Check
33
* @typedef {import('./lib/index.js').Test} Test
4-
* @typedef {import('./lib/index.js').TestFunctionAnything} TestFunctionAnything
5-
*/
6-
7-
/**
8-
* @template {import('hast').Element} T
9-
* @typedef {import('./lib/index.js').AssertPredicate<T>} AssertPredicate
10-
*/
11-
12-
/**
13-
* @template {import('hast').Element} T
14-
* @typedef {import('./lib/index.js').PredicateTest<T>} PredicateTest
15-
*/
16-
17-
/**
18-
* @template {import('hast').Element} T
19-
* @typedef {import('./lib/index.js').TestFunctionPredicate<T>} TestFunctionPredicate
4+
* @typedef {import('./lib/index.js').TestFunction} TestFunction
205
*/
216

227
export {convertElement, isElement} from './lib/index.js'

index.test-d.ts

+179-154
Original file line numberDiff line numberDiff line change
@@ -1,173 +1,198 @@
1-
import type {Element} from 'hast'
2-
import {expectAssignable, expectError, expectNotType, expectType} from 'tsd'
3-
import {unified} from 'unified'
4-
import type {Node} from 'unist'
5-
import {isElement, convertElement} from './index.js'
1+
import type {Element, Parents, Root, RootContent} from 'hast'
2+
import {expectAssignable, expectNotType, expectType} from 'tsd'
3+
import {convertElement, isElement} from './index.js'
64

7-
/* Setup. */
8-
type Section = {
9-
tagName: 'section'
10-
} & Element
5+
// # Setup
116

12-
type Article = {
13-
tagName: 'article'
14-
} & Element
7+
const hastElement = (function (): Root | RootContent {
8+
return {type: 'element', tagName: 'a', properties: {}, children: []}
9+
})()
1510

16-
const section: Element = {
17-
type: 'element',
18-
tagName: 'section',
19-
properties: {},
20-
children: [{type: 'text', value: 'x'}]
11+
const unknownValue = (function (): unknown {
12+
return {type: 'something'}
13+
})()
14+
15+
const someTagName = (function (): string {
16+
return 'c'
17+
})()
18+
19+
// # `isElement`
20+
21+
// No node.
22+
expectType<false>(isElement())
23+
24+
// No test.
25+
expectType<boolean>(isElement(hastElement))
26+
27+
if (isElement(unknownValue)) {
28+
expectType<Element>(unknownValue)
29+
}
30+
31+
/* Nullish test. */
32+
expectType<boolean>(isElement(hastElement, null))
33+
expectType<boolean>(isElement(hastElement, undefined))
34+
35+
// String test.
36+
if (isElement(hastElement, 'a')) {
37+
expectType<Element & {tagName: 'a'}>(hastElement)
2138
}
2239

23-
const article: Element = {
24-
type: 'element',
25-
tagName: 'article',
26-
properties: {},
27-
children: [{type: 'text', value: 'x'}]
40+
if (isElement(hastElement, 'b')) {
41+
expectType<Element & {tagName: 'b'}>(hastElement)
2842
}
2943

30-
const isSection = (element: Element): element is Section =>
31-
element.tagName === 'section'
44+
if (isElement(hastElement, someTagName)) {
45+
expectType<Element>(hastElement)
46+
}
3247

33-
expectType<false>(isElement())
48+
// Function test (with explicit assertion).
49+
expectType<boolean>(isElement(hastElement, isHeading2))
3450

35-
/* Missing parameters. */
36-
expectError(isElement<Section>())
51+
if (isElement(hastElement, isHeading2)) {
52+
expectType<Element & {tagName: 'h2'}>(hastElement)
53+
}
3754

38-
/* Types cannot be narrowed without predicate. */
39-
expectType<boolean>(isElement(section))
55+
if (isElement(hastElement, isParagraph)) {
56+
expectType<Element & {tagName: 'p'}>(hastElement)
57+
}
4058

41-
/* Incorrect generic. */
42-
expectError(isElement<string>(section, 'section'))
43-
expectError(isElement<boolean>(section, 'section'))
44-
expectError(isElement<Record<string, unknown>>(section, 'section'))
59+
if (isElement(hastElement, isParagraph)) {
60+
expectNotType<Element & {tagName: 'h2'}>(hastElement)
61+
}
4562

46-
/* Should be assignable to boolean. */
47-
expectType<boolean>(isElement<Section>(section, 'section'))
63+
if (isElement(hastElement, isHeading2)) {
64+
expectAssignable<Element>(hastElement)
65+
expectType<'h2'>(hastElement.tagName)
66+
}
4867

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))
68+
// Function test (implicit assertion).
69+
expectType<boolean>(isElement(hastElement, isHeading2Loose))
5770

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'))
71+
if (isElement(hastElement, isHeading2Loose)) {
72+
expectNotType<Element & {tagName: 'h2'}>(hastElement)
73+
}
6274

63-
if (isElement<Section>(section, 'section')) {
64-
expectType<Section>(section)
65-
expectNotType<Article>(section)
75+
if (isElement(hastElement, isParagraphLoose)) {
76+
expectNotType<Element & {tagName: 'p'}>(hastElement)
6677
}
6778

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-
expectAssignable<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>())
136-
137-
declare const node: unknown
138-
139-
/* Type assertion */
140-
if (isElement(node)) {
141-
expectType<Element>(node)
142-
}
143-
144-
if (isElement(node, (node): node is Section => node.tagName === 'section')) {
145-
expectType<Section>(node)
146-
}
147-
148-
/**
149-
* This test demonstrates that, while the test definitely asserts that `node`
150-
* is an element, it asserts that it is *some* kind of element.
151-
* If we’d define `node` as an `Element` in the if-branch (which is correct),
152-
* TypeScript will think `node` is *not* an `Element` in the else-branch (which
153-
* is incorrect).
154-
* We can’t solve this in this project, but users can change their code (see
155-
* next example).
156-
*/
157-
if (isElement(node, (node) => node.children.length > 0)) {
158-
expectType<unknown>(node)
159-
} else {
160-
expectType<unknown>(node)
161-
}
79+
if (isElement(hastElement, isHead)) {
80+
expectType<Element>(hastElement)
81+
}
82+
83+
// Array tests.
84+
// Can’t narrow down.
85+
expectType<boolean>(isElement(hastElement, ['h2', isHeading2, isHeading2Loose]))
86+
87+
// Can’t narrow down.
88+
if (isElement(hastElement, ['h2', isHeading2, isHeading2Loose])) {
89+
expectNotType<Element & {tagName: 'h2'}>(hastElement)
90+
}
91+
92+
// # `convertElement`
93+
94+
// No node.
95+
const checkNone = convertElement()
96+
expectType<boolean>(checkNone())
97+
98+
// No test.
99+
expectType<boolean>(checkNone(hastElement))
100+
101+
if (checkNone(unknownValue)) {
102+
expectType<Element>(unknownValue)
103+
}
104+
105+
/* Nullish test. */
106+
const checkNull = convertElement(null)
107+
const checkUndefined = convertElement(null)
108+
expectType<boolean>(checkNull(hastElement))
109+
expectType<boolean>(checkUndefined(hastElement))
110+
111+
// String test.
112+
const checkHeading2 = convertElement('h2')
113+
const checkParagraph = convertElement('p')
114+
115+
if (checkHeading2(hastElement)) {
116+
expectType<Element & {tagName: 'h2'}>(hastElement)
117+
}
118+
119+
if (checkParagraph(hastElement)) {
120+
expectType<Element & {tagName: 'p'}>(hastElement)
121+
}
122+
123+
// Function test (with explicit assertion).
124+
const checkParagraphFn = convertElement(isParagraph)
125+
const checkHeading2Fn = convertElement(isHeading2)
126+
127+
expectType<boolean>(checkHeading2Fn(hastElement))
128+
129+
if (checkHeading2Fn(hastElement)) {
130+
expectType<Element & {tagName: 'h2'}>(hastElement)
131+
}
132+
133+
if (checkParagraphFn(hastElement)) {
134+
expectType<Element & {tagName: 'p'}>(hastElement)
135+
}
136+
137+
if (checkParagraphFn(hastElement)) {
138+
expectNotType<Element & {tagName: 'h2'}>(hastElement)
139+
}
140+
141+
if (checkHeading2Fn(hastElement)) {
142+
expectAssignable<Element>(hastElement)
143+
expectType<'h2'>(hastElement.tagName)
144+
}
145+
146+
// Function test (implicit assertion).
147+
const checkHeading2LooseFn = convertElement(isHeading2Loose)
148+
const checkParagraphLooseFn = convertElement(isParagraphLoose)
149+
const checkHeadFn = convertElement(isHead)
150+
151+
expectType<boolean>(checkHeading2LooseFn(hastElement))
152+
153+
if (checkHeading2LooseFn(hastElement)) {
154+
expectNotType<Element & {tagName: 'h2'}>(hastElement)
155+
}
156+
157+
if (checkParagraphLooseFn(hastElement)) {
158+
expectNotType<Element & {tagName: 'p'}>(hastElement)
159+
}
160+
161+
if (checkHeadFn(hastElement)) {
162+
expectNotType<Element & {tagName: 'h2'}>(hastElement)
163+
}
164+
165+
// Array tests.
166+
// Can’t narrow down.
167+
const isHeadingArray = convertElement(['h2', isHeading2, isHeading2Loose])
168+
169+
expectType<boolean>(isHeadingArray(hastElement))
170+
171+
// Can’t narrow down.
172+
if (isHeadingArray(hastElement)) {
173+
expectNotType<Element & {tagName: 'h2'}>(hastElement)
174+
}
175+
176+
function isHeading2(element: Element): element is Element & {tagName: 'h2'} {
177+
return element.tagName === 'h2'
178+
}
179+
180+
function isHeading2Loose(element: Element) {
181+
return element.tagName === 'h2'
182+
}
183+
184+
function isParagraph(element: Element): element is Element & {tagName: 'p'} {
185+
return element.tagName === 'p'
186+
}
187+
188+
function isParagraphLoose(element: Element) {
189+
return element.tagName === 'p'
190+
}
162191

163-
/**
164-
* This is the suggested use of this package so TypeScript can infer what types
165-
* it’s working with.
166-
* This way, `node` as an `Element` in the if-branch, and it could still be an
167-
* element (or something else) in the else-branch.
168-
*/
169-
if (isElement(node) && node.children.length > 0) {
170-
expectType<Element>(node)
171-
} else {
172-
expectType<unknown>(node)
192+
function isHead(
193+
element: Element,
194+
index: number | undefined,
195+
parent: Parents | undefined
196+
) {
197+
return parent ? index === 0 : false
173198
}

0 commit comments

Comments
 (0)