From 3eb1a5319648ef6617125ac54484cb7e76eeac40 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Wed, 24 Jul 2019 20:53:39 -0700 Subject: [PATCH] Add typings Closes GH-9. Reviewed-by: Junyoung Choi Reviewed-by: Titus Wormer Co-authored-by: Junyoung Choi --- convert.d.ts | 6 ++ index.d.ts | 66 +++++++++++++++ package.json | 13 ++- tsconfig.json | 15 ++++ tslint.json | 15 ++++ unist-util-is-test.ts | 190 ++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 302 insertions(+), 3 deletions(-) create mode 100644 convert.d.ts create mode 100644 index.d.ts create mode 100644 tsconfig.json create mode 100644 tslint.json create mode 100644 unist-util-is-test.ts diff --git a/convert.d.ts b/convert.d.ts new file mode 100644 index 0000000..ce80aea --- /dev/null +++ b/convert.d.ts @@ -0,0 +1,6 @@ +import {Test, TestFunction} from './' +import {Node} from 'unist' + +declare function convert(test: Test): TestFunction + +export = convert diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..f05d502 --- /dev/null +++ b/index.d.ts @@ -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['type'] + + /** + * Check that some attributes on a node are matched + * + * @typeParam T type of node that passes test + */ + type TestObject = Partial + + /** + * 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 = ( + 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 = TestType | TestObject | TestFunction +} + +/** + * 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( + node: unknown, + test: unistUtilIs.Test | Array>, + index?: number, + parent?: Parent, + context?: any +): node is T + +export = unistUtilIs diff --git a/package.json b/package.json index 25193ac..704ea5d 100644 --- a/package.json +++ b/package.json @@ -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, diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..3d5c8e7 --- /dev/null +++ b/tsconfig.json @@ -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"] + } + } +} diff --git a/tslint.json b/tslint.json new file mode 100644 index 0000000..aa59581 --- /dev/null +++ b/tslint.json @@ -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 + } +} diff --git a/unist-util-is-test.ts b/unist-util-is-test.ts new file mode 100644 index 0000000..c3c0235 --- /dev/null +++ b/unist-util-is-test.ts @@ -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() +// $ExpectError +is(heading) + +/*=== invalid generic ===*/ +// $ExpectError +is(heading, 'heading') +// $ExpectError +is(heading, 'heading') +// $ExpectError +is<{}>(heading, 'heading') + +/*=== assignable to boolean ===*/ +const wasItAHeading: boolean = is(heading, 'heading') + +/*=== type string test ===*/ +is(heading, 'heading') +is(element, 'heading') +// $ExpectError +is(heading, 'element') + +if (is(heading, 'heading')) { + const maybeHeading: Heading = heading + // $ExpectError + const maybeNotHeading: Element = heading +} + +is(element, 'element') +is(heading, 'element') +// $ExpectError +is(element, 'heading') + +if (is(element, 'element')) { + const maybeElement: Element = element + // $ExpectError + const maybeNotElement: Heading = element +} + +/*=== type predicate function test ===*/ +is(heading, isHeading) +is(element, isHeading) +// $ExpectError +is(heading, isElement) + +if (is(heading, isHeading)) { + const maybeHeading: Heading = heading + // $ExpectError + const maybeNotHeading: Element = heading +} + +is(element, isElement) +is(heading, isElement) +// $ExpectError +is(element, isHeading) + +if (is(element, isElement)) { + const maybeElement: Element = element + // $ExpectError + const maybeNotElement: Heading = element +} + +/*=== type object test ===*/ +is(heading, {type: 'heading', depth: 2}) +is(element, {type: 'heading', depth: 2}) +// $ExpectError +is(heading, {type: 'heading', depth: '2'}) + +if (is(heading, {type: 'heading', depth: 2})) { + const maybeHeading: Heading = heading + // $ExpectError + const maybeNotHeading: Element = heading +} + +is(element, {type: 'element', tagName: 'section'}) +is(heading, {type: 'element', tagName: 'section'}) +// $ExpectError +is(element, {type: 'element', tagName: true}) + +if (is(element, {type: 'element', tagName: 'section'})) { + const maybeElement: Element = element + // $ExpectError + const maybeNotElement: Heading = element +} + +/*=== type array of tests ===*/ +is(heading, [ + 'heading', + isElement, + {type: 'ParagraphNode'} +]) +if ( + is(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(tree, 'heading')) { + // do something + } + return tree +}) + +/*=== convert ===*/ +convert('heading') +// $ExpectError +convert('element') +convert({type: 'heading', depth: 2}) +// $ExpectError +convert({type: 'heading', depth: 2}) +convert(isHeading) +// $ExpectError +convert(isHeading)