diff --git a/index.d.ts b/index.d.ts new file mode 100644 index 0000000..6792eb8 --- /dev/null +++ b/index.d.ts @@ -0,0 +1,101 @@ +/** + * Notes about these type definitions: + * + * - Callbacks returning multiple completion values using multiple arguments are not supported by these types. + * Prefer to use Node's style by grouping your values in a single object or array. + * Support for this kind of callback is blocked by Microsoft/TypeScript#5453 + * + * - For ease of use, `asyncDone` lets you pass callback functions with a result type `T` instead of `T | undefined`. + * This matches Node's types but can lead to unsound code being typechecked. + * + * The following code typechecks but fails at runtime: + * ```typescript + * async function getString(): Promise { + * return "Hello, World!"; + * } + * + * async function evilGetString(): Promise { + * throw new Error("Hello, World!"); + * } + * + * function cb(err: Error | null, result: string): void { + * // This is unsound because `result` is `undefined` when `err` is not `null`. + * console.log(result.toLowerCase()); + * } + * + * asyncDone(getString, cb); // Prints `hello, world!` + * asyncDone(evilGetString, cb); // Runtime error: `TypeError: Cannot read property 'toLowerCase' of undefined` + * ``` + * + * Enforcing stricter callbacks would require developers to use `result?: string` and assert the existence + * of the result either by checking it directly or using the `!` assertion operator after testing for errors. + * ```typescript + * function stricterCb1(err: Error | null, result?: string): void { + * if (err !== null) { + * console.error(err); + * return; + * } + * console.log(result!.toLowerCase()); + * } + * + * function stricterCb2(err: Error | null, result?: string): void { + * if (result === undefined) { + * console.error("Undefined result. Error:); + * console.error(err); + * return; + * } + * console.log(result.toLowerCase()); + * } + * ``` + */ +import { ChildProcess } from "child_process"; +import { EventEmitter } from "events"; +import { Readable as ReadableStream } from "stream"; + +declare namespace asyncDone { + + /** + * Represents a callback function used to signal the completion of a + * task that does not return any completion value. + */ + type VoidCallback = (err: Error | null) => void; + + /** + * Represents a callback function used to signal the completion of a + * task that does return a single completion value. + */ + interface Callback { + (err: null, result: T): void; + + // Set the type of `result` to `T` if you want to enforce stricter callback functions. + // (See comment above about risks of unsound type checking) + (err: Error, result?: any): void; + } + + /** + * Minimal `Observable` interface compatible with `async-done`. + * + * @see https://github.com/ReactiveX/rxjs/blob/c3c56867eaf93f302ac7cd588034c7d8712f2834/src/internal/Observable.ts#L77 + */ + interface Observable { + subscribe(next?: (value: T) => void, error?: (error: any) => void, complete?: () => void): any; + } + + /** + * Represents an async operation. + */ + export type AsyncTask = + ((done: VoidCallback) => void) + | ((done: Callback) => void) + | (() => ChildProcess | EventEmitter | Observable | PromiseLike | ReadableStream ); +} + +/** + * Takes a function to execute (`fn`) and a function to call on completion (`callback`). + * + * @param fn Function to execute. + * @param callback Function to call on completion. + */ +declare function asyncDone(fn: asyncDone.AsyncTask, callback: asyncDone.Callback): void; + +export = asyncDone; diff --git a/package.json b/package.json index eebaad2..52769be 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "scripts": { "lint": "eslint . && jscs index.js test/", "pretest": "npm run lint", - "test": "mocha --async-only", + "test": "mocha --async-only && npm run test-types", + "test-types": "tsc -p test/types", "cover": "istanbul cover _mocha --report lcovonly", "coveralls": "npm run cover && istanbul-coveralls" }, @@ -32,6 +33,7 @@ "stream-exhaust": "^1.0.1" }, "devDependencies": { + "@types/node": "^9.3.0", "eslint": "^1.7.3", "eslint-config-gulp": "^2.0.0", "expect": "^1.19.0", @@ -41,8 +43,9 @@ "jscs-preset-gulp": "^1.0.0", "mocha": "^2.4.5", "pumpify": "^1.3.6", - "rx": "^4.0.6", + "rxjs": "^5.5.6", "through2": "^2.0.0", + "typescript": "^2.6.2", "when": "^3.7.3" }, "keywords": [ diff --git a/test/observables.js b/test/observables.js index 797b994..a468eb3 100644 --- a/test/observables.js +++ b/test/observables.js @@ -4,14 +4,14 @@ var expect = require('expect'); var asyncDone = require('../'); -var Observable = require('rx').Observable; +var Observable = require('rxjs').Observable; function success() { return Observable.empty(); } function successValue() { - return Observable.return(42); + return Observable.of(42); } function failure() { diff --git a/test/types/callback.ts b/test/types/callback.ts new file mode 100644 index 0000000..17a0651 --- /dev/null +++ b/test/types/callback.ts @@ -0,0 +1,44 @@ +import asyncDone, { Callback } from "async-done"; + +function success(cb: Callback): void { + cb(null, 2); +} + +function failure(cb: Callback): void { + cb(new Error("Callback Error")); +} + +function neverDone(): number { + return 2; +} + +// `success` and stricter callback +asyncDone(success, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// The following code fails to compile as expected: +// asyncDone(success, function (err: Error | null, result?: string): void { +// console.log("Done"); +// }); + +// `success` and unsound callback +asyncDone(success, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// `failure` and stricter callback +asyncDone(failure, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `failure` and unsound callback +asyncDone(failure, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// I don't think TS is currently able to prevent the current code from compiling +// (`neverDone` matches with `(done: VoidCallback) => void` for example) +// asyncDone(neverDone, function(err: Error | null, result?: number): void { +// console.log("Done"); +// }); diff --git a/test/types/child_processes.ts b/test/types/child_processes.ts new file mode 100644 index 0000000..d0d431b --- /dev/null +++ b/test/types/child_processes.ts @@ -0,0 +1,19 @@ +import asyncDone from "async-done"; +import cp from "child_process"; + + +function success(): cp.ChildProcess { + return cp.exec("echo hello world"); +} + +function failure(): cp.ChildProcess { + return cp.exec("foo-bar-baz hello world"); +} + +asyncDone(success, function (err: Error | null): void { + console.log("Done"); +}); + +asyncDone(failure, function (err: Error | null): void { + console.log("Done"); +}); diff --git a/test/types/observables.ts b/test/types/observables.ts new file mode 100644 index 0000000..4f0dd53 --- /dev/null +++ b/test/types/observables.ts @@ -0,0 +1,46 @@ +import asyncDone from "async-done"; +import { Observable } from "rxjs/Observable"; +import 'rxjs/add/observable/empty'; +import 'rxjs/add/observable/of'; + +function success(): Observable { + return Observable.empty(); +} + +function successValue(): Observable { + return Observable.of(42); +} + +function failure(): Observable { + return Observable.throw(new Error("Observable error")); +} + +// `success` callback +asyncDone(success, function (err: Error | null): void { + console.log("Done"); +}); + +// The following code fails to compile as expected (`undefined` is not assignable to `number`): +// asyncDone(success, function (err: Error | null, result: number): void { +// console.log("Done"); +// }); + +// `successValue` and stricter callback +asyncDone(successValue, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `successValue` and unsound callback +asyncDone(successValue, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// `failure` and stricter callback +asyncDone(failure, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `failure` and unsound callback +asyncDone(failure, function (err: Error | null, result: number): void { + console.log("Done"); +}); diff --git a/test/types/promises.ts b/test/types/promises.ts new file mode 100644 index 0000000..e7a7498 --- /dev/null +++ b/test/types/promises.ts @@ -0,0 +1,34 @@ +import asyncDone from "async-done"; + +function success(): Promise { + return Promise.resolve(2); +} + +function failure(): Promise { + return Promise.reject(new Error("Promise Error")); +} + +// `successValue` and stricter callback +asyncDone(success, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// The following code fails to compile as expected: +// asyncDone(success, function (err: Error | null, result?: string): void { +// console.log("Done"); +// }); + +// `successValue` and unsound callback +asyncDone(success, function (err: Error | null, result: number): void { + console.log("Done"); +}); + +// `failure` and stricter callback +asyncDone(failure, function (err: Error | null, result?: number): void { + console.log("Done"); +}); + +// `failure` and unsound callback +asyncDone(failure, function (err: Error | null, result: number): void { + console.log("Done"); +}); diff --git a/test/types/streams.ts b/test/types/streams.ts new file mode 100644 index 0000000..a3aed30 --- /dev/null +++ b/test/types/streams.ts @@ -0,0 +1,34 @@ +import asyncDone from "async-done"; +import { Readable, Stream } from "stream"; + +function readableSuccess(): Readable { + return undefined as any; +} + +function readableFail(): Readable { + return undefined as any; +} + +function streamSuccess(): Stream { + return undefined as any; +} + +function streamFail(): Stream { + return undefined as any; +} + +asyncDone(readableSuccess, function (err: Error | null): void { + console.log("Done"); +}); + +asyncDone(readableFail, function (err: Error | null): void { + console.log("Done"); +}); + +asyncDone(streamSuccess, function (err: Error | null): void { + console.log("Done"); +}); + +asyncDone(streamFail, function (err: Error | null): void { + console.log("Done"); +}); diff --git a/test/types/tsconfig.json b/test/types/tsconfig.json new file mode 100644 index 0000000..35ef644 --- /dev/null +++ b/test/types/tsconfig.json @@ -0,0 +1,63 @@ +{ + "compilerOptions": { + "allowJs": false, + "allowSyntheticDefaultImports": true, + "allowUnreachableCode": false, + "allowUnusedLabels": false, + "alwaysStrict": true, + "baseUrl": "../..", + "charset": "utf8", + "checkJs": false, + "declaration": true, + "disableSizeLimit": false, + "downlevelIteration": false, + "emitBOM": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "forceConsistentCasingInFileNames": true, + "importHelpers": false, + "inlineSourceMap": false, + "inlineSources": false, + "isolatedModules": false, + "lib": [ + "es2017" + ], + "locale": "en-us", + "module": "commonjs", + "moduleResolution": "node", + "newLine": "lf", + "noEmit": true, + "noEmitHelpers": false, + "noEmitOnError": true, + "noErrorTruncation": true, + "noFallthroughCasesInSwitch": true, + "noImplicitAny": true, + "noImplicitReturns": true, + "noImplicitThis": true, + "noStrictGenericChecks": false, + "noUnusedLocals": false, + "noUnusedParameters": false, + "noLib": false, + "noResolve": false, + "paths": { + "async-done": [ + "index.d.ts" + ] + }, + "preserveConstEnums": true, + "removeComments": false, + "rootDir": "", + "skipLibCheck": false, + "sourceMap": true, + "strict": true, + "strictNullChecks": true, + "suppressExcessPropertyErrors": false, + "suppressImplicitAnyIndexErrors": false, + "target": "es2017", + "traceResolution": false + }, + "include": [ + "./**/*.ts" + ], + "exclude": [] +} diff --git a/test/types/various.ts b/test/types/various.ts new file mode 100644 index 0000000..73c6956 --- /dev/null +++ b/test/types/various.ts @@ -0,0 +1,5 @@ +import asyncDone, {AsyncTask, VoidCallback} from "async-done"; + +// Do not error if the return value is not `void`. +const fn: AsyncTask = (cb: VoidCallback): NodeJS.Timer => setTimeout(cb, 1000); +asyncDone(fn, () => {});