Skip to content

Commit

Permalink
Add typings
Browse files Browse the repository at this point in the history
Closes GH-9.

Reviewed-by: Junyoung Choi <fluke8259@gmail.com>
Reviewed-by: Titus Wormer <tituswormer@gmail.com>

Co-authored-by: Junyoung Choi <fluke8259@gmail.com>
  • Loading branch information
2 people authored and wooorm committed Jul 26, 2019
1 parent aee63b5 commit 3eb1a53
Show file tree
Hide file tree
Showing 6 changed files with 302 additions and 3 deletions.
6 changes: 6 additions & 0 deletions convert.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import {Test, TestFunction} from './'
import {Node} from 'unist'

declare function convert<T extends Node>(test: Test<T>): TestFunction<T>

export = convert
66 changes: 66 additions & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// TypeScript Version: 3.5

import {Node, Parent} from 'unist'

declare namespace unistUtilIs {
/**
* Check that type property matches expectation for a node
*
* @typeParam T type of node that passes test
*/
type TestType<T extends Node> = T['type']

/**
* Check that some attributes on a node are matched
*
* @typeParam T type of node that passes test
*/
type TestObject<T extends Node> = Partial<T>

/**
* Check if a node passes a test
*
* @param node node to check
* @param index index of node in parent
* @param parent parent of node
* @typeParam T type of node that passes test
* @returns true if type T is found, false otherwise
*/
type TestFunction<T extends Node> = (
node: unknown,
index?: number,
parent?: Parent
) => node is T

/**
* Union of all the types of tests
*
* @typeParam T type of node that passes test
*/
type Test<T extends Node> = TestType<T> | TestObject<T> | TestFunction<T>
}

/**
* Unist utility to check if a node passes a test.
*
* @param node Node to check.
* @param test When not given, checks if `node` is a `Node`.
* When `string`, works like passing `function (node) {return node.type === test}`.
* When `function` checks if function passed the node is true.
* When `object`, checks that all keys in test are in node, and that they have (strictly) equal values.
* When `array`, checks any one of the subtests pass.
* @param index Position of `node` in `parent`
* @param parent Parent of `node`
* @param context Context object to invoke `test` with
* @typeParam T type that node is compared with
* @returns Whether test passed and `node` is a `Node` (object with `type` set to non-empty `string`).
*/
declare function unistUtilIs<T extends Node>(
node: unknown,
test: unistUtilIs.Test<T> | Array<unistUtilIs.Test<any>>,
index?: number,
parent?: Parent,
context?: any
): node is T

export = unistUtilIs
13 changes: 10 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,27 +21,34 @@
],
"files": [
"index.js",
"convert.js"
"convert.js",
"index.d.ts",
"convert.d.ts"
],
"types": "index.d.ts",
"dependencies": {},
"devDependencies": {
"browserify": "^16.0.0",
"dtslint": "^0.9.0",
"nyc": "^14.0.0",
"prettier": "^1.0.0",
"remark-cli": "^6.0.0",
"remark-preset-wooorm": "^5.0.0",
"tape": "^4.0.0",
"tinyify": "^2.0.0",
"typescript": "^3.5.3",
"unified": "^8.3.2",
"xo": "^0.24.0"
},
"scripts": {
"format": "remark . -qfo && prettier --write \"**/*.js\" && xo --fix",
"format": "remark . -qfo && prettier --write \"**/*.{js,ts}\" && xo --fix",
"build-bundle": "browserify . -s unistUtilIs > unist-util-is.js",
"build-mangle": "browserify . -s unistUtilIs -p tinyify > unist-util-is.min.js",
"build": "npm run build-bundle && npm run build-mangle",
"test-api": "node test",
"test-coverage": "nyc --reporter lcov tape test.js",
"test": "npm run format && npm run build && npm run test-coverage"
"test-types": "dtslint .",
"test": "npm run format && npm run build && npm run test-coverage && npm run test-types"
},
"prettier": {
"tabWidth": 2,
Expand Down
15 changes: 15 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"compilerOptions": {
"lib": ["es2015"],
"strict": true,
"noImplicitAny": true,
"noImplicitThis": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"baseUrl": ".",
"paths": {
"unist-util-is": ["index.d.ts"],
"unist-util-is/convert": ["convert.d.ts"]
}
}
}
15 changes: 15 additions & 0 deletions tslint.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"extends": "dtslint/dtslint.json",
"rules": {
"callable-types": false,
"max-line-length": false,
"no-redundant-jsdoc": false,
"no-void-expression": false,
"only-arrow-functions": false,
"semicolon": false,
"unified-signatures": false,
"whitespace": false,
"interface-over-type-literal": false,
"no-unnecessary-generics": false
}
}
190 changes: 190 additions & 0 deletions unist-util-is-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
import {Node, Parent} from 'unist'
import unified = require('unified')
import is = require('unist-util-is')
import convert = require('unist-util-is/convert')

/*=== setup ===*/
interface Heading extends Parent {
type: 'heading'
depth: number
children: Node[]
}

interface Element extends Parent {
type: 'element'
tagName: string
properties: {
[key: string]: unknown
}
content: Node
children: Node[]
}

interface Paragraph extends Parent {
type: 'ParagraphNode'
}

const heading: Node = {
type: 'heading',
depth: 2,
children: []
}

const element: Node = {
type: 'element',
tagName: 'section',
properties: {},
content: {type: 'text'},
children: []
}

const isHeading = (node: unknown): node is Heading =>
typeof node === 'object' && node !== null && (node as Node).type === 'heading'
const isElement = (node: unknown): node is Element =>
typeof node === 'object' && node !== null && (node as Node).type === 'element'

/*=== types cannot be narrowed without predicate ===*/
// $ExpectError
const maybeHeading: Heading = heading
// $ExpectError
const maybeElement: Element = element

/*=== missing params ===*/
// $ExpectError
is()
// $ExpectError
is<Node>()
// $ExpectError
is<Node>(heading)

/*=== invalid generic ===*/
// $ExpectError
is<string>(heading, 'heading')
// $ExpectError
is<boolean>(heading, 'heading')
// $ExpectError
is<{}>(heading, 'heading')

/*=== assignable to boolean ===*/
const wasItAHeading: boolean = is<Heading>(heading, 'heading')

/*=== type string test ===*/
is<Heading>(heading, 'heading')
is<Heading>(element, 'heading')
// $ExpectError
is<Heading>(heading, 'element')

if (is<Heading>(heading, 'heading')) {
const maybeHeading: Heading = heading
// $ExpectError
const maybeNotHeading: Element = heading
}

is<Element>(element, 'element')
is<Element>(heading, 'element')
// $ExpectError
is<Element>(element, 'heading')

if (is<Element>(element, 'element')) {
const maybeElement: Element = element
// $ExpectError
const maybeNotElement: Heading = element
}

/*=== type predicate function test ===*/
is(heading, isHeading)
is(element, isHeading)
// $ExpectError
is<Heading>(heading, isElement)

if (is(heading, isHeading)) {
const maybeHeading: Heading = heading
// $ExpectError
const maybeNotHeading: Element = heading
}

is(element, isElement)
is(heading, isElement)
// $ExpectError
is<Element>(element, isHeading)

if (is(element, isElement)) {
const maybeElement: Element = element
// $ExpectError
const maybeNotElement: Heading = element
}

/*=== type object test ===*/
is<Heading>(heading, {type: 'heading', depth: 2})
is<Heading>(element, {type: 'heading', depth: 2})
// $ExpectError
is<Heading>(heading, {type: 'heading', depth: '2'})

if (is<Heading>(heading, {type: 'heading', depth: 2})) {
const maybeHeading: Heading = heading
// $ExpectError
const maybeNotHeading: Element = heading
}

is<Element>(element, {type: 'element', tagName: 'section'})
is<Element>(heading, {type: 'element', tagName: 'section'})
// $ExpectError
is<Element>(element, {type: 'element', tagName: true})

if (is<Element>(element, {type: 'element', tagName: 'section'})) {
const maybeElement: Element = element
// $ExpectError
const maybeNotElement: Heading = element
}

/*=== type array of tests ===*/
is<Heading | Element | Paragraph>(heading, [
'heading',
isElement,
{type: 'ParagraphNode'}
])
if (
is<Heading | Element | Paragraph>(heading, [
'heading',
isElement,
{type: 'ParagraphNode'}
])
) {
switch (heading.type) {
case 'heading': {
heading // $ExpectType Heading
break
}
case 'element': {
heading // $ExpectType Element
break
}
case 'ParagraphNode': {
heading // $ExpectType Paragraph
break
}
// $ExpectError
case 'dne': {
break
}
}
}

/*=== usable in unified transform ===*/
unified().use(() => tree => {
if (is<Heading>(tree, 'heading')) {
// do something
}
return tree
})

/*=== convert ===*/
convert<Heading>('heading')
// $ExpectError
convert<Heading>('element')
convert<Heading>({type: 'heading', depth: 2})
// $ExpectError
convert<Element>({type: 'heading', depth: 2})
convert<Heading>(isHeading)
// $ExpectError
convert<Element>(isHeading)

0 comments on commit 3eb1a53

Please sign in to comment.