diff --git a/lib/compose.d.ts b/lib/compose.d.ts index a3d0cc2..19f7d09 100644 --- a/lib/compose.d.ts +++ b/lib/compose.d.ts @@ -1,41 +1,61 @@ -export declare type ComposableTarget = - CallableFunction & ((this: C1, ...args: I1) => Promise); +import { Promise } from "./promise"; +export declare type ComposableFunction = + ((this: C1, ...args: I1) => Promise); + +/** + * If you see this error message when building your code, it's likely that + * you're adding a function to a composition that is destined to fail. + */ export type UnreachableFunctionWarning = "Your function will never be run"; -export declare type ComposableSafeFunction = - [R1] extends [never] ? UnreachableFunctionWarning : ((this: C1, val: R1) => R2 | Promise) +export declare type SafeComposableFunction = + [R1] extends [never] + ? UnreachableFunctionWarning + : ComposableFunction; -export declare type ComposableUnsafeFunction = - [R1] extends [never] ? UnreachableFunctionWarning : ((this: C1, val: R1) => Promise) +export declare type UnsafeComposableFunction = + [R1] extends [never] + ? UnreachableFunctionWarning + : ComposableFunction; -export interface SafeComposable extends ComposableTarget { - then(fn: ComposableUnsafeFunction): - [E1] extends [never] ? never : UnsafeComposable; +/** + * + */ +export interface SafeComposition extends ComposableFunction { + then(fn: SafeComposableFunction): + SafeComposition; - then(fn: ComposableSafeFunction): - SafeComposable; + then(fn: UnsafeComposableFunction): + UnsafeComposition; - catch: never; + catch<__, C2 extends C1 = C1>(fn: SafeComposableFunction): never; } -export interface UnsafeComposable extends ComposableTarget { - __ErrorTypeCheck__: [E1] extends [never] ? 'Please use a "SafeComposable"' : E1; - - then(fn: ComposableUnsafeFunction): - UnsafeComposable; +/** + * + */ +export interface UnsafeComposition extends ComposableFunction { + then(fn: SafeComposableFunction): + UnsafeComposition; - then(fn: ComposableSafeFunction): - UnsafeComposable; + then(fn: UnsafeComposableFunction): + UnsafeComposition; - catch(fn: ComposableUnsafeFunction): - [E2] extends [never] ? never : UnsafeComposable; + catch<__, C2 extends C1 = C1>(fn: SafeComposableFunction): + SafeComposition; - catch<__, C2 extends C1 = C1>(fn: ComposableSafeFunction): - SafeComposable; + catch(fn: UnsafeComposableFunction): + UnsafeComposition; } -export declare function compose(fn: ComposableTarget): +export declare function resolve(val: T): Promise; +export declare function resolve(): Promise; + +export declare function reject(val: T): Promise; +export declare function reject(): Promise; + +export declare function compose(fn: ComposableFunction): [E1] extends [never] - ? SafeComposable - : UnsafeComposable; + ? SafeComposition + : UnsafeComposition; diff --git a/lib/compose.js b/lib/compose.js index 9400b66..a188567 100644 --- a/lib/compose.js +++ b/lib/compose.js @@ -6,7 +6,7 @@ * * @param fn - The function to compose */ -function compose(fn) { +export function compose(fn) { return Object.setPrototypeOf(function (...args) { return fn.apply(this, args); }, compose); @@ -26,4 +26,5 @@ compose.catch = function (fn) { }); } -exports.compose = compose; +export const resolve = Promise.resolve.bind(Promise); +export const reject = Promise.reject.bind(Promise); diff --git a/lib/compose.test.js b/lib/compose.test.js index 408824e..7de46c4 100644 --- a/lib/compose.test.js +++ b/lib/compose.test.js @@ -2,12 +2,17 @@ "use strict"; /** - * @typedef {import('./compose').SafeComposable} SafeComposable + * @typedef {import('./compose').ComposableFunction} ComposableFunction + * @template C1, I1, R1, E1 + */ + +/** + * @typedef {import('./compose').SafeComposition} SafeComposition * @template C1, R1, R2 */ /** - * @typedef {import('./compose').UnsafeComposable} UnsafeComposable + * @typedef {import('./compose').UnsafeComposition} UnsafeComposition * @template C1, R1, R2, E1 */ @@ -16,77 +21,167 @@ */ /** - * @typedef {import('./compose').ComposableSafeFunction} ComposableSafeFunction + * @typedef {import('./compose').SafeComposableFunction} SafeComposableFunction * @template C1, R1, R2 */ /** - * @typedef {import('./compose').ComposableUnsafeFunction} ComposableUnsafeFunction + * @typedef {import('./compose').UnsafeComposableFunction} UnsafeComposableFunction * @template C1, R1, R2, E1 */ -const { compose } = require('./compose'); +import assert from 'node:assert'; +import test from 'node:test'; +import { compose, resolve, reject } from './compose.js'; +import { Promise } from './promise.js'; /** * Maintaining safe compositions. */ { - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition1 = compose(safeTarget); - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition2 = composition1.then(safeWrapArray); - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition3 = composition2.then(safeUnwrapArray); - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition4 = composition3.then(safeStringLength); + + test('Maintaining safe compositions', async () => { + await composition4('pizza', 'tomato').then((val) => { + assert.equal(val, 11); + }); + + await composition4('cheese', 'spinach').then((val) => { + assert.equal(val, 13); + }); + + await composition4('mushroom', 'pineapple').then((val) => { + assert.equal(val, 17); + }); + }); } /** * Adding errors to originally safe compositions. */ { - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition1 = compose(safeTarget); - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition2 = composition1.then(safeStringLength); - /** @type {UnsafeComposable} */ - const composition3 = composition2.then(unsafeGeneric); + /** @type {UnsafeComposition} */ + const composition3 = composition2.then(unsafeIdentity); + + test('Adding errors to originally safe compositions', async () => { + await composition3('pizza', 'tomato').then((val) => { + assert.equal(val, 11); + }); + + await composition3('cheese', 'spinach').then((val) => { + assert.equal(val, 13); + }); + + await composition3('mushroom', 'pineapple').then((val) => { + assert.equal(val, 17); + }); + }); } /** * Keeping track of errors in originally unsafe compositions. */ { - /** @type {UnsafeComposable} */ + /** @type {UnsafeComposition} */ const composition1 = compose(unsafeTarget); - /** @type {UnsafeComposable} */ + /** @type {UnsafeComposition} */ const composition2 = composition1.then(safeWrapArray); - /** @type {UnsafeComposable} */ + /** @type {UnsafeComposition} */ const composition3 = composition2.then(safeUnwrapArray); - /** @type {UnsafeComposable} */ + /** @type {UnsafeComposition} */ const composition4 = composition3.then(safeStringLength); + + test('Keeping track of errors in originally unsafe compositions', async () => { + await composition4('pizza', 'tomato').then((val) => { + assert.equal(val, 11); + }); + + await composition4('cheese', 'spinach').then((val) => { + assert.equal(val, 13); + }); + + await composition4('mushroom', 'pineapple').then((val) => { + assert.equal(val, 17); + }); + }); } /** * Discarding errors in originally unsafe compositions. */ { - /** @type {UnsafeComposable} */ + /** @type {UnsafeComposition} */ const composition1 = compose(unsafeTarget); - /** @type {UnsafeComposable} */ + /** @type {UnsafeComposition} */ const composition2 = composition1.then(safeStringLength); - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition3 = composition2.catch(resolveErrors); + + test('Keeping track of errors in originally unsafe compositions', async () => { + await composition3('pizza', 'tomato').then((val) => { + assert.equal(val, 11); + }); + + await composition3('cheese', 'spinach').then((val) => { + assert.equal(val, 13); + }); + + await composition3('mushroom', 'pineapple').then((val) => { + assert.equal(val, 17); + }); + }); +} + +/** + * Stacking compositions + */ +{ + /** @type {SafeComposition} */ + const composition1 = compose(safeTarget); + + /** @type {SafeComposition} */ + const composition2 = compose(safeWrapArray); + + /** @type {SafeComposition} */ + const composition3 = compose(safeUnwrapArray); + + /** @type {SafeComposition} */ + const composition4 = composition1.then(composition2).then(composition3); + + test('Stacking compositions', async () => { + await composition4('pizza', 'tomato').then((val) => { + assert.equal(val, 'pizzatomato'); + }); + + await composition4('cheese', 'spinach').then((val) => { + assert.equal(val, 'cheesespinach'); + }); + + await composition4('mushroom', 'pineapple').then((val) => { + assert.equal(val, 'mushroompineapple'); + }); + }); } /** @@ -96,10 +191,10 @@ const { compose } = require('./compose'); * typed. */ { - /** @type {SafeComposable} */ + /** @type {SafeComposition} */ const composition1 = compose(safeTarget); - /** @type {(fn: ComposableSafeFunction) => any} */ + /** @type {(fn: SafeComposableFunction) => any} */ const _ = composition1.then /** @type {(fn: UnreachableFunctionWarning) => any} */ @@ -113,13 +208,13 @@ const { compose } = require('./compose'); * ensure that "fail only" compositions are possible. */ { - /** @type {UnsafeComposable} */ + /** @type {UnsafeComposition} */ const composition1 = compose(brokenTarget); /** @type {(fn: UnreachableFunctionWarning) => any} */ const _ = composition1.then - /** @type {(fn: ComposableUnsafeFunction) => any} */ + /** @type {(fn: UnsafeComposableFunction) => any} */ const __ = composition1.catch } @@ -129,7 +224,7 @@ const { compose } = require('./compose'); * @returns {Promise} */ function safeTarget(input1, input2) { - return Promise.resolve(input1 + input2); + return resolve(input1 + input2); } /** @@ -138,7 +233,7 @@ function safeTarget(input1, input2) { * @returns {Promise} */ function unsafeTarget(input1, input2) { - return Promise.resolve(input1 + input2); + return resolve(input1 + input2); } /** @@ -147,7 +242,7 @@ function unsafeTarget(input1, input2) { * @returns {Promise} */ function brokenTarget(input1, input2) { - return Promise.reject(input1 + input2); + return reject(input1 + input2); } /** @@ -155,7 +250,7 @@ function brokenTarget(input1, input2) { * @returns {Promise<[string], never>} */ function safeWrapArray(input) { - return Promise.resolve([input]); + return resolve([input]); } /** @@ -163,7 +258,7 @@ function safeWrapArray(input) { * @returns {Promise} */ function safeUnwrapArray([input]) { - return Promise.resolve(input); + return resolve(input); } /** @@ -171,7 +266,7 @@ function safeUnwrapArray([input]) { * @returns {Promise} */ function safeStringLength(input) { - return Promise.resolve(input.length); + return resolve(input.length); } /** @@ -179,8 +274,8 @@ function safeStringLength(input) { * @param {T} input * @returns {Promise} */ -function unsafeGeneric(input) { - return Promise.resolve(input); +function unsafeIdentity(input) { + return resolve(input); } /** diff --git a/types/lib-es2015/promise.d.ts b/lib/promise.d.ts similarity index 70% rename from types/lib-es2015/promise.d.ts rename to lib/promise.d.ts index 6e7803f..2ef559e 100644 --- a/types/lib-es2015/promise.d.ts +++ b/lib/promise.d.ts @@ -1,15 +1,14 @@ /** - * A strongly typed extension to the Promise interface, allowing for type - * assertions on both the resolved and rejected values. - */ -interface Promise { - then( - onfulfilled?: ((_: T) => RT | Promise) | null, - onrejected?: ((_: E) => RT | Promise) | null, - ): Promise; + * A strongly typed extension to the Promise interface, allowing for type + * assertions on both the resolved and rejected values. + */ +export interface Promise { + then( + onfulfilled: ((_: T) => RT | Promise) | null, + ): Promise; catch( - onrejected?: ((_: E) => RT | Promise) | null, + onrejected: ((_: E) => RT | Promise) | null, ): Promise; } diff --git a/lib/promise.js b/lib/promise.js new file mode 100644 index 0000000..f6510f0 --- /dev/null +++ b/lib/promise.js @@ -0,0 +1,4 @@ +// @ts-nocheck +"use strict"; + +export const Promise = globalThis.Promise; diff --git a/package-lock.json b/package-lock.json index 7e5599b..3d16c7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,21 +1,23 @@ { "name": "@emphori/compose", + "version": "0.0.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@emphori/compose", + "version": "0.0.1", "license": "MIT", - "dependencies": { - "@typescript/lib-es2015": "file:./types/lib-es2015" - }, "devDependencies": { + "@types/node": "^18.6.2", "typescript": "^4.6.2" } }, - "node_modules/@typescript/lib-es2015": { - "resolved": "types/lib-es2015", - "link": true + "node_modules/@types/node": { + "version": "18.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.2.tgz", + "integrity": "sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==", + "dev": true }, "node_modules/typescript": { "version": "4.6.2", @@ -29,11 +31,16 @@ "node": ">=4.2.0" } }, - "types/lib-es2015": {} + "types/lib-es2015": { + "extraneous": true + } }, "dependencies": { - "@typescript/lib-es2015": { - "version": "file:types/lib-es2015" + "@types/node": { + "version": "18.6.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.6.2.tgz", + "integrity": "sha512-KcfkBq9H4PI6Vpu5B/KoPeuVDAbmi+2mDBqGPGUgoL7yXQtcWGu2vJWmmRkneWK3Rh0nIAX192Aa87AqKHYChQ==", + "dev": true }, "typescript": { "version": "4.6.2", diff --git a/package.json b/package.json index af42329..4776e42 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,6 @@ { "name": "@emphori/compose", + "version": "0.0.1", "description": "A lightweight functional composition helper", "keywords": [], "author": "Emphori (https://emphori.co)", @@ -8,19 +9,26 @@ ], "homepage": "https://github.com/emphori/compose#readme", "license": "MIT", - "main": "lib/compose.js", - "types": "lib/compose.d.ts", + "type": "module", + "exports": { + ".": { + "import": "./lib/compose.js" + }, + "./promise": { + "import": "./lib/promise.js" + } + }, "files": [ + "lib/compose.d.ts", "lib/compose.js", - "lib/compose.d.ts" + "lib/promise.d.ts", + "lib/promise.js" ], "scripts": { - "test": "tsc --noEmit --checkJs --strict --target es6 lib/*.js" - }, - "dependencies": { - "@typescript/lib-es2015": "file:./types/lib-es2015" + "test": "tsc --noEmit --checkJs --strict --allowSyntheticDefaultImports --target es6 lib/*.js" }, "devDependencies": { + "@types/node": "^18.6.2", "typescript": "^4.6.2" }, "publishConfig": { diff --git a/types/index.d.ts b/types/index.d.ts new file mode 100644 index 0000000..8b12574 --- /dev/null +++ b/types/index.d.ts @@ -0,0 +1,9 @@ +/** + * @todo Extract this "AssertType" type to its own library + */ +type AssertType = any + +/** + * @todo Extract this "AssertExtends" type to its own library + */ +type AssertExtends = any