Skip to content

Commit

Permalink
Add support for inferring type of parents
Browse files Browse the repository at this point in the history
  • Loading branch information
wooorm committed Jul 5, 2023
1 parent 323984d commit adf39ed
Show file tree
Hide file tree
Showing 2 changed files with 230 additions and 186 deletions.
310 changes: 151 additions & 159 deletions index.test-d.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,50 @@
import {expectAssignable, expectNotType, expectType} from 'tsd'
import type {Literal, Node, Parent} from 'unist'
import {is} from 'unist-util-is'
import type {
Blockquote,
Content,
Definition,
Delete,
Emphasis,
Footnote,
FootnoteDefinition,
Heading,
Link,
LinkReference,
List,
ListItem,
Paragraph,
PhrasingContent,
Root,
Strong,
Table,
TableCell,
TableRow
} from 'mdast'
import type {Node, Parent} from 'unist'
import {CONTINUE, EXIT, SKIP, visitParents} from './index.js'

// To do: use `mdast` when released.
type Nodes = Root | Content

// To do: use `mdast` when released.
type Parents =
| Blockquote
| Delete
| Emphasis
| Footnote
| FootnoteDefinition
| Heading
| Link
| LinkReference
| List
| ListItem
| Paragraph
| Root
| Strong
| Table
| TableCell
| TableRow

/* Setup */
const implicitTree = {
type: 'root',
Expand All @@ -14,187 +56,144 @@ const sampleTree: Root = {
children: [{type: 'heading', depth: 1, children: []}]
}

const complexTree: Root = {
type: 'root',
children: [
{
type: 'blockquote',
children: [{type: 'paragraph', children: [{type: 'text', value: 'a'}]}]
},
{
type: 'paragraph',
children: [
{
type: 'emphasis',
children: [{type: 'emphasis', children: [{type: 'text', value: 'b'}]}]
},
{type: 'text', value: 'c'}
]
}
]
}

interface Element extends Parent {
type: 'element'
tagName: string
properties: Record<string, unknown>
content: Node
children: Array<Node>
}

type Content = Flow | Phrasing

interface Root extends Parent {
type: 'root'
children: Array<Flow>
}

type Flow = Blockquote | Heading | Paragraph

interface Blockquote extends Parent {
type: 'blockquote'
children: Array<Flow>
}

interface Heading extends Parent {
type: 'heading'
depth: number
children: Array<Phrasing>
}

interface Paragraph extends Parent {
type: 'paragraph'
children: Array<Phrasing>
}

type Phrasing = Text | Emphasis

interface Emphasis extends Parent {
type: 'emphasis'
children: Array<Phrasing>
}

interface Text extends Literal {
type: 'text'
value: string
}

const isNode = (node: unknown): node is Node =>
const isNode = (node: unknown): node is Nodes =>
typeof node === 'object' && node !== null && 'type' in node
const headingTest = (node: unknown): node is Heading =>
isNode(node) && node.type === 'heading'
const elementTest = (node: unknown): node is Element =>
isNode(node) && node.type === 'element'
const paragraphTest = (node: unknown): node is Paragraph =>
isNode(node) && node.type === 'paragraph'

/* Missing params. */
// @ts-expect-error check.
// ## Missing parameters
// @ts-expect-error: check that `node` is passed.
visitParents()
// @ts-expect-error check.
// @ts-expect-error: check that `visitor` is passed.
visitParents(sampleTree)

/* Visit without test. */
// ## No test
visitParents(sampleTree, function (node, parents) {
expectType<Root | Content>(node)
expectType<Array<Root | Blockquote | Heading | Paragraph | Emphasis>>(parents)
expectType<Nodes>(node)
expectType<Array<Parents>>(parents)
})
visitParents(implicitTree, function (node) {

visitParents(implicitTree, function (node, parents) {
// Objects are too loose.
expectAssignable<Node>(node)
expectNotType<Node>(node) // Objects are too loose.
expectNotType<Node>(node)
expectAssignable<Array<Node>>(parents)
})

/* Visit with type test. */
// ## String test

// Knows it’s a heading and its parents.
visitParents(sampleTree, 'heading', function (node, parents) {
expectType<Heading>(node)
// Note that most of these can’t be a parent of `Heading`, but still.
expectType<Array<Root | Blockquote | Heading | Paragraph | Emphasis>>(parents)
expectType<
Array<Blockquote | FootnoteDefinition | List | ListItem | Root>
>(parents)
})
visitParents(sampleTree, 'element', function (node) {
// Not in tree.

// Not in tree.
visitParents(sampleTree, 'element', function (node, parents) {
expectType<never>(node)
expectType<Array<never>>(parents)
})
// @ts-expect-error check.
visitParents(sampleTree, 'heading', function (_: Element) {})
visitParents(implicitTree, 'heading', function (node) {
expectType<never>(node) // Objects are too loose.
expectAssignable<Heading>(node)
expectNotType<Heading>(node) // Objects are too loose.

// Implicit nodes are too loose.
visitParents(implicitTree, 'heading', function (node, parents) {
expectType<never>(node)
expectType<Array<never>>(parents)
})

/* Visit with object test. */
// ## Props test

// Knows that headings have depth, but TS doesn’t infer the depth normally.
visitParents(sampleTree, {depth: 1}, function (node) {
expectType<Heading>(node)
expectType<1 | 2 | 3 | 4 | 5 | 6>(node.depth)
})
visitParents(sampleTree, {random: 'property'} as const, function (node) {
expectType<never>(node)

// This goes fine.
visitParents(sampleTree, {type: 'heading'} as const, function (node) {
expectType<Heading>(node)
expectType<1 | 2 | 3 | 4 | 5 | 6>(node.depth)
})
visitParents(
sampleTree,
{type: 'heading', depth: '1'} as const,
function (node) {
// Not in tree.
expectType<never>(node)
}
)
visitParents(sampleTree, {tagName: 'section'} as const, function (node) {
// Not in tree.

// For some reason the const goes wrong.
visitParents(sampleTree, {depth: 1} as const, function (node) {
// Note: something going wrong here, to do: investigate.
expectType<never>(node)
})

// @ts-expect-error check.
visitParents(sampleTree, {type: 'heading'} as const, function (_: Element) {})
visitParents(implicitTree, {type: 'heading'} as const, function (node) {
expectType<never>(node) // Objects are too loose.
expectAssignable<Heading>(node)
expectNotType<Heading>(node) // Objects are too loose.
// For some reason the const goes wrong.
visitParents(sampleTree, {type: 'heading', depth: 1} as const, function (node) {
// Note: something going wrong here, to do: investigate.
expectType<never>(node)
})

/* Visit with function test. */
visitParents(sampleTree, headingTest, function (node) {
expectType<Heading>(node)
// Function test (implicit assertion).
visitParents(sampleTree, isHeadingLoose, function (node) {
expectType<Nodes>(node)
})
// @ts-expect-error check.
visitParents(sampleTree, headingTest, function (_: Element) {})
visitParents(sampleTree, elementTest, function (node) {
// Not in tree.
expectType<never>(node)
// Function test (explicit assertion).
visitParents(sampleTree, isHeading, function (node) {
expectType<Heading>(node)
expectType<1 | 2 | 3 | 4 | 5 | 6>(node.depth)
})
visitParents(implicitTree, headingTest, function (node) {
expectType<never>(node) // Objects are too loose.
expectAssignable<Heading>(node)
expectNotType<Heading>(node) // Objects are too loose.
// Function test (explicit assertion).
visitParents(sampleTree, isHeading2, function (node) {
expectType<Heading & {depth: 2}>(node)
})

/* Visit with array of tests. */
visitParents(sampleTree, ['heading', {depth: 1}, headingTest], function (node) {
// ## Combined tests
visitParents(sampleTree, ['heading', {depth: 1}, isHeading], function (node) {
// Unfortunately TS casts things in arrays too vague.
expectType<Root | Content>(node)
})

/* Visit returns action. */
// To do: update to `unist-util-is` should make this work?
// visitParents(
// sampleTree,
// ['heading', {depth: 1}, isHeading] as const,
// function (node) {
// // Unfortunately TS casts things in arrays too vague.
// expectType<Root | Content>(node)
// }
// )

// ## Return type: incorrect.
// @ts-expect-error: not an action.
visitParents(sampleTree, function () {
return CONTINUE
return 'random'
})
// @ts-expect-error: not a tuple: missing action.
visitParents(sampleTree, function () {
return EXIT
return [1]
})
// @ts-expect-error: not a tuple: incorrect action.
visitParents(sampleTree, function () {
return SKIP
return ['random', 1]
})

// @ts-expect-error check.
// ## Return type: action.
visitParents(sampleTree, function () {
return 'random'
return CONTINUE
})
visitParents(sampleTree, function () {
return EXIT
})
visitParents(sampleTree, function () {
return SKIP
})

/* Visit returns index. */
// ## Return type: index.
visitParents(sampleTree, function () {
return 0
})
visitParents(sampleTree, function () {
return 1
})

/* Visit returns tuple. */
// ## Return type: tuple.
visitParents(sampleTree, function () {
return [CONTINUE, 1]
})
Expand All @@ -208,40 +207,33 @@ visitParents(sampleTree, function () {
return [SKIP]
})

// @ts-expect-error check.
visitParents(sampleTree, function () {
return [1]
})

// @ts-expect-error check.
visitParents(sampleTree, function () {
return ['random', 1]
// ## Infer on tree
visitParents(sampleTree, 'tableCell', function (node) {
visitParents(node, function (node, parents) {
expectType<TableCell | PhrasingContent>(node)
expectType<
Array<
Delete | Emphasis | Footnote | Link | LinkReference | Strong | TableCell
>
>(parents)
})
})

/* Should infer children from the given tree. */
visitParents(complexTree, function (node) {
expectType<Root | Content>(node)
visitParents(sampleTree, 'definition', function (node) {
visitParents(node, function (node, parents) {
expectType<Definition>(node)
expectType<Array<never>>(parents)
})
})

const blockquote = complexTree.children[0]
if (is<Blockquote>(blockquote, 'blockquote')) {
visitParents(blockquote, function (node) {
expectType<Content>(node)
})
function isHeading(node: Node): node is Heading {
return node ? node.type === 'heading' : false
}

const paragraph = complexTree.children[1]
if (is<Paragraph>(paragraph, 'paragraph')) {
visitParents(paragraph, function (node) {
expectType<Paragraph | Phrasing>(node)
})

const child = paragraph.children[1]
function isHeading2(node: Node): node is Heading & {depth: 2} {
return isHeading(node) && node.depth === 2
}

if (is<Emphasis>(child, 'emphasis')) {
visitParents(child, 'blockquote', function (node) {
// `blockquote` does not exist in phrasing.
expectType<never>(node)
})
}
function isHeadingLoose(node: Node) {
return node ? node.type === 'heading' : false
}
Loading

0 comments on commit adf39ed

Please sign in to comment.