From b5d7ed13e8083329e232e0791275964931474ea0 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Sun, 21 Jul 2019 13:52:01 -0700 Subject: [PATCH 1/4] types: add typings for unist-util-is typings are based off types available in definitely typed, and have been updated to add type predicate support, meaning when `unist-util-is` is used as an `if` conditional, everything inside the conditional scope can use the narrowed type. Co-authored-by: Junyoung Choi --- package.json | 12 ++- types/index.d.ts | 88 ++++++++++++++++++++++ types/tsconfig.json | 14 ++++ types/tslint.json | 15 ++++ types/unist-util-is-test.ts | 145 ++++++++++++++++++++++++++++++++++++ 5 files changed, 271 insertions(+), 3 deletions(-) create mode 100644 types/index.d.ts create mode 100644 types/tsconfig.json create mode 100644 types/tslint.json create mode 100644 types/unist-util-is-test.ts diff --git a/package.json b/package.json index 25193ac..af55d59 100644 --- a/package.json +++ b/package.json @@ -21,27 +21,33 @@ ], "files": [ "index.js", - "convert.js" + "convert.js", + "types/index.d.ts" ], + "types": "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 types", + "test": "npm run format && npm run build && npm run test-coverage && npm run test-types" }, "prettier": { "tabWidth": 2, diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..dbd1b00 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,88 @@ +// 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, + index?: number, + parent?: Parent, + context?: any +): node is 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 + * @returns Whether test passed and `node` is a `Node` (object with `type` set to non-empty `string`). + */ +declare function unistUtilIs( + node: unknown, + test: Array>, + index?: number, + parent?: Parent, + context?: any +): node is Node + +export = unistUtilIs diff --git a/types/tsconfig.json b/types/tsconfig.json new file mode 100644 index 0000000..76091fc --- /dev/null +++ b/types/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "lib": ["es2015"], + "strict": true, + "noImplicitAny": true, + "noImplicitThis": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "baseUrl": ".", + "paths": { + "unist-util-is": ["index.d.ts"] + } + } +} diff --git a/types/tslint.json b/types/tslint.json new file mode 100644 index 0000000..aa59581 --- /dev/null +++ b/types/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/types/unist-util-is-test.ts b/types/unist-util-is-test.ts new file mode 100644 index 0000000..ea219bd --- /dev/null +++ b/types/unist-util-is-test.ts @@ -0,0 +1,145 @@ +import {Node, Parent} from 'unist' +import unified = require('unified') +import is = require('unist-util-is') + +/*=== 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[] +} + +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'}]) +is(element, ['heading', isElement, {type: 'ParagraphNode'}]) + +/*=== usable in unified transform ===*/ +unified().use(() => tree => { + if (is(tree, 'heading')) { + // do something + } + return tree +}) From b4017e38f19ea0674d7850ec7f06805951611578 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Mon, 22 Jul 2019 10:05:22 -0700 Subject: [PATCH 2/4] types: add typing for convert Co-authored-by: Junyoung Choi --- package.json | 3 ++- types/convert.d.ts | 6 ++++++ types/tsconfig.json | 3 ++- types/unist-util-is-test.ts | 12 ++++++++++++ 4 files changed, 22 insertions(+), 2 deletions(-) create mode 100644 types/convert.d.ts diff --git a/package.json b/package.json index af55d59..2e82eb8 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "files": [ "index.js", "convert.js", - "types/index.d.ts" + "types/index.d.ts", + "types/convert.d.ts" ], "types": "types/index.d.ts", "dependencies": {}, diff --git a/types/convert.d.ts b/types/convert.d.ts new file mode 100644 index 0000000..ce80aea --- /dev/null +++ b/types/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/types/tsconfig.json b/types/tsconfig.json index 76091fc..3d5c8e7 100644 --- a/types/tsconfig.json +++ b/types/tsconfig.json @@ -8,7 +8,8 @@ "strictFunctionTypes": true, "baseUrl": ".", "paths": { - "unist-util-is": ["index.d.ts"] + "unist-util-is": ["index.d.ts"], + "unist-util-is/convert": ["convert.d.ts"] } } } diff --git a/types/unist-util-is-test.ts b/types/unist-util-is-test.ts index ea219bd..9ce7457 100644 --- a/types/unist-util-is-test.ts +++ b/types/unist-util-is-test.ts @@ -1,6 +1,7 @@ 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 { @@ -143,3 +144,14 @@ unified().use(() => tree => { } return tree }) + +/*=== convert ===*/ +convert('heading') +// $ExpectError +convert('element') +convert({type: 'heading', depth: 2}) +// $ExpectError +convert({type: 'heading', depth: 2}) +convert(isHeading) +// $ExpectError +convert(isHeading) From e69b5293947041fd3f832fe6e41bc6c26d0f18d8 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Mon, 22 Jul 2019 21:15:16 -0700 Subject: [PATCH 3/4] types: support generic when an array of tests are passed --- types/index.d.ts | 24 +----------------------- types/unist-util-is-test.ts | 37 +++++++++++++++++++++++++++++++++++-- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index dbd1b00..f05d502 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -57,32 +57,10 @@ declare namespace unistUtilIs { */ declare function unistUtilIs( node: unknown, - test: unistUtilIs.Test, + test: unistUtilIs.Test | Array>, index?: number, parent?: Parent, context?: any ): node is 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 - * @returns Whether test passed and `node` is a `Node` (object with `type` set to non-empty `string`). - */ -declare function unistUtilIs( - node: unknown, - test: Array>, - index?: number, - parent?: Parent, - context?: any -): node is Node - export = unistUtilIs diff --git a/types/unist-util-is-test.ts b/types/unist-util-is-test.ts index 9ce7457..c3c0235 100644 --- a/types/unist-util-is-test.ts +++ b/types/unist-util-is-test.ts @@ -20,6 +20,10 @@ interface Element extends Parent { children: Node[] } +interface Paragraph extends Parent { + type: 'ParagraphNode' +} + const heading: Node = { type: 'heading', depth: 2, @@ -134,8 +138,37 @@ if (is(element, {type: 'element', tagName: 'section'})) { } /*=== type array of tests ===*/ -is(heading, ['heading', isElement, {type: 'ParagraphNode'}]) -is(element, ['heading', isElement, {type: 'ParagraphNode'}]) +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 => { From b54b26709593f3a869ab1d3d1884fca804f20e51 Mon Sep 17 00:00:00 2001 From: Christian Murphy Date: Wed, 24 Jul 2019 07:31:55 -0700 Subject: [PATCH 4/4] types: flatten typings so convert typing will be recognized Co-authored-by: Junyoung Choi --- types/convert.d.ts => convert.d.ts | 0 types/index.d.ts => index.d.ts | 0 package.json | 8 ++++---- types/tsconfig.json => tsconfig.json | 0 types/tslint.json => tslint.json | 0 types/unist-util-is-test.ts => unist-util-is-test.ts | 0 6 files changed, 4 insertions(+), 4 deletions(-) rename types/convert.d.ts => convert.d.ts (100%) rename types/index.d.ts => index.d.ts (100%) rename types/tsconfig.json => tsconfig.json (100%) rename types/tslint.json => tslint.json (100%) rename types/unist-util-is-test.ts => unist-util-is-test.ts (100%) diff --git a/types/convert.d.ts b/convert.d.ts similarity index 100% rename from types/convert.d.ts rename to convert.d.ts diff --git a/types/index.d.ts b/index.d.ts similarity index 100% rename from types/index.d.ts rename to index.d.ts diff --git a/package.json b/package.json index 2e82eb8..704ea5d 100644 --- a/package.json +++ b/package.json @@ -22,10 +22,10 @@ "files": [ "index.js", "convert.js", - "types/index.d.ts", - "types/convert.d.ts" + "index.d.ts", + "convert.d.ts" ], - "types": "types/index.d.ts", + "types": "index.d.ts", "dependencies": {}, "devDependencies": { "browserify": "^16.0.0", @@ -47,7 +47,7 @@ "build": "npm run build-bundle && npm run build-mangle", "test-api": "node test", "test-coverage": "nyc --reporter lcov tape test.js", - "test-types": "dtslint types", + "test-types": "dtslint .", "test": "npm run format && npm run build && npm run test-coverage && npm run test-types" }, "prettier": { diff --git a/types/tsconfig.json b/tsconfig.json similarity index 100% rename from types/tsconfig.json rename to tsconfig.json diff --git a/types/tslint.json b/tslint.json similarity index 100% rename from types/tslint.json rename to tslint.json diff --git a/types/unist-util-is-test.ts b/unist-util-is-test.ts similarity index 100% rename from types/unist-util-is-test.ts rename to unist-util-is-test.ts