diff --git a/dist/index.d.ts b/dist/index.d.ts index 49d6aa2..a2b078c 100644 --- a/dist/index.d.ts +++ b/dist/index.d.ts @@ -1,6 +1,6 @@ import type { Promisify } from './interface'; export * from './interface'; -export declare function async(getPromise: () => PromiseLike): Promisify; -export declare function async(getPromise: () => Data): Promisify; -export declare function async(promise: PromiseLike): Promisify; -export declare function async(data: Data): Promisify; +export declare function async(getPromise: () => PromiseLike): Promisify; +export declare function async(getPromise: () => T): Promisify; +export declare function async(promise: PromiseLike): Promisify; +export declare function async(data: T): Promisify; diff --git a/dist/index.js b/dist/index.js index 67750fa..1f6462b 100644 --- a/dist/index.js +++ b/dist/index.js @@ -1,39 +1,3 @@ -var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { - function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } - return new (P || (P = Promise))(function (resolve, reject) { - function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } - function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } - function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } - step((generator = generator.apply(thisArg, _arguments || [])).next()); - }); -}; -var __generator = (this && this.__generator) || function (thisArg, body) { - var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; - return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; - function verb(n) { return function (v) { return step([n, v]); }; } - function step(op) { - if (f) throw new TypeError("Generator is already executing."); - while (_) try { - if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; - if (y = 0, t) op = [op[0] & 2, t.value]; - switch (op[0]) { - case 0: case 1: t = op; break; - case 4: _.label++; return { value: op[1], done: false }; - case 5: _.label++; y = op[1]; op = [0]; continue; - case 7: op = _.ops.pop(); _.trys.pop(); continue; - default: - if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } - if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } - if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } - if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } - if (t[2]) _.ops.pop(); - _.trys.pop(); continue; - } - op = body.call(thisArg, _); - } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } - if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; - } -}; var __read = (this && this.__read) || function (o, n) { var m = typeof Symbol === "function" && o[Symbol.iterator]; if (!m) return o; @@ -59,65 +23,40 @@ var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { } return to.concat(ar || Array.prototype.slice.call(from)); }; -import { cast } from './utils'; +import { cast, proxymify, isFunction } from './utils'; export * from './interface'; +/** + * The function allows you to work flatly with promises using the `Proxy` object. + * + * The value you pass will be patched using the `Promisify` type in such a way that + * each of its members will be wrapped in a promise. However, you can still work with this value + * without worrying about the nested promises. + * + * @param value + * Can be any value or a function that returns any value. + * The final value will be wrapped in the `Promise`. + * + * @example + * ```typescript + * function getData(): Promise[]> { + * return Promise.resolve([Promise.resolve(21)]); + * } + * + * // "21" + * const str1 = await flatAsync(getData)()[0].toFixed(1); + * // "21" + * const str2 = await flatAsync(getData())[0].toFixed(1); + * ``` + */ export function async(value) { - if (typeof value === 'function') { - return cast(proxymify(cast(value))); + if (isFunction(value)) { + return cast(proxymify(function () { + var args = []; + for (var _i = 0; _i < arguments.length; _i++) { + args[_i] = arguments[_i]; + } + return Promise.resolve(value.apply(void 0, __spreadArray([], __read(args), false))); + })); } return cast(proxymify(function () { return Promise.resolve(value); })); } -function proxymify(getData) { - var promise = getData(); - return new Proxy(getData, { - get: function (_, prop) { - var _a; - return (_a = handleNativePromise(promise, prop)) !== null && _a !== void 0 ? _a : proxymifyNextValue(promise, prop); - }, - apply: function (target, _, args) { - return proxymifyNextValueFromFunctionCall(cast(target), args); - }, - }); -} -function handleNativePromise(promise, prop) { - if (!Object.hasOwn(Promise.prototype, prop)) - return; - var value = promise[cast(prop)]; - if (typeof value === 'function') { - return value.bind(promise); - } - return value; -} -function proxymifyNextValue(promise, prop) { - return proxymify(function () { return getNextValueFromPrevPromise(promise, prop); }); -} -function getNextValueFromPrevPromise(promise, prop) { - return __awaiter(this, void 0, void 0, function () { - var data, value; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, promise]; - case 1: - data = _a.sent(); - value = data[cast(prop)]; - return [2 /*return*/, typeof value === 'function' ? value.bind(data) : value]; - } - }); - }); -} -function proxymifyNextValueFromFunctionCall(getFn, args) { - return proxymify(function () { return getNextValueFromFunction(getFn, args); }); -} -function getNextValueFromFunction(getFn, args) { - return __awaiter(this, void 0, void 0, function () { - var fn; - return __generator(this, function (_a) { - switch (_a.label) { - case 0: return [4 /*yield*/, getFn()]; - case 1: - fn = _a.sent(); - return [2 /*return*/, fn.apply(void 0, __spreadArray([], __read(args), false))]; - } - }); - }); -} diff --git a/dist/interface.d.ts b/dist/interface.d.ts index 4c3230a..101538e 100644 --- a/dist/interface.d.ts +++ b/dist/interface.d.ts @@ -1,36 +1,151 @@ -declare type Fn = (...args: A) => Promisify; -declare type AnyFunction = (...params: any[]) => any; -declare type Overloads = T extends () => infer R ? () => Promisify : T extends { +/** + * Promisifies each function overload return type + */ +declare type Overloads = T extends () => infer R ? T extends (...args: infer A) => any ? (...args: A) => Promisify : () => Promisify : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; (...args: infer A5): infer R5; (...args: infer A6): infer R6; -} ? Fn | Fn | Fn | Fn | Fn | Fn : T extends { + (...args: infer A7): infer R7; + (...args: infer A8): infer R8; + (...args: infer A9): infer R9; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; + (...args: A7): Promisify; + (...args: A8): Promisify; + (...args: A9): Promisify; +} : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; (...args: infer A5): infer R5; -} ? Fn | Fn | Fn | Fn | Fn : T extends { + (...args: infer A6): infer R6; + (...args: infer A7): infer R7; + (...args: infer A8): infer R8; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; + (...args: A7): Promisify; + (...args: A8): Promisify; +} : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; -} ? Fn | Fn | Fn | Fn : T extends { + (...args: infer A5): infer R5; + (...args: infer A6): infer R6; + (...args: infer A7): infer R7; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; + (...args: A7): Promisify; +} : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; -} ? Fn | Fn | Fn : T extends { + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; + (...args: infer A6): infer R6; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; +} : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; -} ? Fn | Fn : T extends (...args: infer A1) => infer R1 ? Fn : never; -declare type UnionToIntersection = (Fn extends any ? (fn: Fn) => void : never) extends (fn: infer F) => void ? F : never; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; +} : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; +} : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; +} : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; +} : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; +} ? { + (...args: A0): Promisify; + (...args: A1): Promisify; +} : T extends (...args: infer A) => infer R ? (...args: A) => Promisify : never; +/** + * Adds `Promise` properties to the specified value + */ declare type WithPromise = Wrapped & Promise; -declare type GetSchema = Value extends string ? String : Value extends number ? Number : Value extends boolean ? Boolean : Value extends bigint ? BigInt : Value extends PromiseLike ? GetSchema : Value extends any[] ? WithPromise : Value; -declare type PromisifySchema = WithPromise<{ - [Key in keyof Schema]: Schema[Key] extends AnyFunction ? UnionToIntersection> : Promisify; +/** + * Maps primitive values to their object representation and "unwraps" `PromiseLike` objects + */ +declare type GetSchema = Value extends string ? String : Value extends number ? Number : Value extends boolean ? Boolean : Value extends bigint ? BigInt : Value extends symbol ? Symbol : Value extends PromiseLike ? GetSchema : Value; +/** + * Promisifies members of the specified schema by creating an object with the promisified properties + * or promisifying return type of each function overload + */ +declare type PromisifySchema = Schema extends (...args: any[]) => any ? Overloads : WithPromise<{ + [Key in keyof Schema]: Promisify; }, Origin>; +/** + * Patches all members of the specified value in such a way that + * each of them will be wrapped in a promise but at the same time preserving its own properties + */ export declare type Promisify = PromisifySchema, Value>; export {}; diff --git a/dist/utils.d.ts b/dist/utils.d.ts index 37c515f..9f9c035 100644 --- a/dist/utils.d.ts +++ b/dist/utils.d.ts @@ -1 +1,21 @@ export declare function cast(value: any): T; +/** + * Checks if provided value is a type of function + * + * @param value + */ +export declare function isFunction(value: any): value is ((...args: any[]) => any); +/** + * The function implements the logic of chaining promises flatly using the `Proxy` object. + * It creates a chain of promises where each next promise takes a value from the previous one. + * + * @param getPrevPromiseLike - the function that returns the previous `PromiseLike` in chain + */ +export declare function proxymify(getPrevPromiseLike: (...args: unknown[]) => PromiseLike): unknown; +declare type Fn = (() => V extends T ? 1 : 0); +declare type AreEquals = Fn extends Fn ? unknown : never; +/** + * Util for checking two types equality + */ +export declare function expectType>(): void; +export {}; diff --git a/dist/utils.js b/dist/utils.js index 773cb27..cc3bb11 100644 --- a/dist/utils.js +++ b/dist/utils.js @@ -1,4 +1,151 @@ +var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { + function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } + function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } + function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); +}; +var __generator = (this && this.__generator) || function (thisArg, body) { + var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; + return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; + function verb(n) { return function (v) { return step([n, v]); }; } + function step(op) { + if (f) throw new TypeError("Generator is already executing."); + while (_) try { + if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; + if (y = 0, t) op = [op[0] & 2, t.value]; + switch (op[0]) { + case 0: case 1: t = op; break; + case 4: _.label++; return { value: op[1], done: false }; + case 5: _.label++; y = op[1]; op = [0]; continue; + case 7: op = _.ops.pop(); _.trys.pop(); continue; + default: + if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } + if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } + if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } + if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } + if (t[2]) _.ops.pop(); + _.trys.pop(); continue; + } + op = body.call(thisArg, _); + } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } + if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; + } +}; +var __read = (this && this.__read) || function (o, n) { + var m = typeof Symbol === "function" && o[Symbol.iterator]; + if (!m) return o; + var i = m.call(o), r, ar = [], e; + try { + while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value); + } + catch (error) { e = { error: error }; } + finally { + try { + if (r && !r.done && (m = i["return"])) m.call(i); + } + finally { if (e) throw e.error; } + } + return ar; +}; +var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) { + if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) { + if (ar || !(i in from)) { + if (!ar) ar = Array.prototype.slice.call(from, 0, i); + ar[i] = from[i]; + } + } + return to.concat(ar || Array.prototype.slice.call(from)); +}; // eslint-disable-next-line @typescript-eslint/no-explicit-any export function cast(value) { return value; } +/** + * Checks if provided value is a type of function + * + * @param value + */ +export function isFunction(value) { + return typeof value === 'function'; +} +/** + * The function implements the logic of chaining promises flatly using the `Proxy` object. + * It creates a chain of promises where each next promise takes a value from the previous one. + * + * @param getPrevPromiseLike - the function that returns the previous `PromiseLike` in chain + */ +export function proxymify(getPrevPromiseLike) { + return new Proxy(getPrevPromiseLike, { + get: function (_, nextProp) { + var _a; + var prevPromiseLike = getPrevPromiseLike(); + return (_a = handleNativePromise(prevPromiseLike, nextProp)) !== null && _a !== void 0 ? _a : proxymifyNextValue(prevPromiseLike, nextProp); + }, + apply: function (target, _, args) { + return proxymifyNextValueFromMethodCall(target, args); + } + }); +} +/** + * Checks if the passed prop is in the `Promise.prototype` and tries to get value by this prop. + * + * @param prevPromiseLike - previous `PromiseLike` + * @param nextProp - possible key from `Promise.prototype` + */ +function handleNativePromise(prevPromiseLike, nextProp) { + if (!Object.hasOwn(Promise.prototype, nextProp)) { + return; + } + var value = prevPromiseLike[cast(nextProp)]; + return isFunction(value) ? value.bind(prevPromiseLike) : value; +} +/** + * Creates next promise in chain that gets a value from the previous one by accessing it using the specified prop. + * + * @param prevPromiseLike - previous `PromiseLike` + * @param nextProp - key to get next value + */ +function proxymifyNextValue(prevPromiseLike, nextProp) { + var _this = this; + return proxymify(function () { return __awaiter(_this, void 0, void 0, function () { + var data, value; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, prevPromiseLike]; + case 1: + data = _a.sent(); + value = cast(data)[cast(nextProp)]; + return [2 /*return*/, isFunction(value) ? value.bind(data) : value]; + } + }); + }); }); +} +/** + * Creates next promise in chain that gets value from the previous one by calling the function. + * This function is called when we try to call a method on the previous proxied object. + * + * @param getMethod - the function that returns `PromiseLike` with currently calling method + * @param args - arguments for the method + */ +function proxymifyNextValueFromMethodCall(getMethod, args) { + var _this = this; + return proxymify(function () { return __awaiter(_this, void 0, void 0, function () { + var method; + return __generator(this, function (_a) { + switch (_a.label) { + case 0: return [4 /*yield*/, getMethod()]; + case 1: + method = _a.sent(); + return [2 /*return*/, method.apply(void 0, __spreadArray([], __read(args), false))]; + } + }); + }); }); +} +/** + * Util for checking two types equality + */ +// eslint-disable-next-line @typescript-eslint/no-empty-function +export function expectType() { } diff --git a/package.json b/package.json index 38b9237..d4ce5cd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "async", - "version": "1.0.0", + "version": "1.0.1", "description": "library for working with async data", "typings": "./dist/index.d.ts", "main": "./dist/index.js", @@ -31,5 +31,6 @@ "lint-staged": "13.0.3", "ts-jest": "29.0.3", "typescript": "4.8.4" - } + }, + "packageManager": "yarn@1.22.22" } diff --git a/src/index.ts b/src/index.ts index 9bcf4ad..0523a03 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,66 +1,46 @@ import type { Promisify } from './interface'; -import { cast } from './utils'; +import { cast, proxymify, isFunction } from './utils'; export * from './interface'; -export function async(getPromise: () => PromiseLike): Promisify; - -export function async(getPromise: () => Data): Promisify; - -export function async(promise: PromiseLike): Promisify; - -export function async(data: Data): Promisify; - -export function async(value: Data | PromiseLike | (() => PromiseLike)): Promisify { - if (typeof value === 'function') { - return cast(proxymify(cast(value))); +export function async(getPromise: () => PromiseLike): Promisify; + +export function async(getPromise: () => T): Promisify; + +export function async(promise: PromiseLike): Promisify; + +export function async(data: T): Promisify; + +/** + * The function allows you to work flatly with promises using the `Proxy` object. + * + * The value you pass will be patched using the `Promisify` type in such a way that + * each of its members will be wrapped in a promise. However, you can still work with this value + * without worrying about the nested promises. + * + * @param value + * Can be any value or a function that returns any value. + * The final value will be wrapped in the `Promise`. + * + * @example + * ```typescript + * function getData(): Promise[]> { + * return Promise.resolve([Promise.resolve(21)]); + * } + * + * // "21" + * const str1 = await flatAsync(getData)()[0].toFixed(1); + * // "21" + * const str2 = await flatAsync(getData())[0].toFixed(1); + * ``` + */ +export function async(value: T | PromiseLike | ((...args: any[]) => any)): Promisify { + if (isFunction(value)) { + return cast(proxymify( + (...args: unknown[]) => Promise.resolve(value(...args)) + )); } return cast(proxymify(() => Promise.resolve(value))); } - -function proxymify(getData: () => PromiseLike): unknown { - const promise = getData(); - - return new Proxy(getData, { - get(_, prop) { - return handleNativePromise(promise, prop) ?? proxymifyNextValue(promise, prop); - }, - - apply(target, _, args) { - return proxymifyNextValueFromFunctionCall(cast(target), args); - }, - }); -} - -function handleNativePromise(promise: PromiseLike, prop: string | symbol) { - if (!Object.hasOwn(Promise.prototype, prop)) return; - - const value = promise[cast>(prop)]; - - if (typeof value === 'function') { - return value.bind(promise); - } - - return value; -} - -function proxymifyNextValue(promise: PromiseLike, prop: string | symbol) { - return proxymify(() => getNextValueFromPrevPromise(promise, prop)); -} - -async function getNextValueFromPrevPromise(promise: PromiseLike, prop: string | symbol): Promise { - const data = await promise; - const value = data[cast(prop)]; - return typeof value === 'function' ? value.bind(data) : value; -} - -function proxymifyNextValueFromFunctionCall(getFn: () => Promise, args: unknown[]) { - return proxymify(() => getNextValueFromFunction(getFn, args)); -} - -async function getNextValueFromFunction(getFn: () => Promise, args: unknown[]) { - const fn = await getFn(); - return fn(...args); -} diff --git a/src/interface.ts b/src/interface.ts index d7fd9ff..cf6a351 100644 --- a/src/interface.ts +++ b/src/interface.ts @@ -1,80 +1,179 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ -type Fn = < - R extends Return = Return, - A extends Args = Args, ->(...args: A) => Promisify; +/** + * Promisifies each function overload return type + */ +type Overloads = T extends () => infer R + ? T extends (...args: infer A) => any + ? (...args: A) => Promisify + : () => Promisify -type AnyFunction = (...params: any[]) => any; + : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; + (...args: infer A6): infer R6; + (...args: infer A7): infer R7; + (...args: infer A8): infer R8; + (...args: infer A9): infer R9; + } + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; + (...args: A7): Promisify; + (...args: A8): Promisify; + (...args: A9): Promisify; + } -type Overloads = T extends () => infer R - ? () => Promisify + : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; + (...args: infer A6): infer R6; + (...args: infer A7): infer R7; + (...args: infer A8): infer R8; + } + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; + (...args: A7): Promisify; + (...args: A8): Promisify; + } : T extends { - (...args: infer A1): infer R1; - (...args: infer A2): infer R2; - (...args: infer A3): infer R3; - (...args: infer A4): infer R4; - (...args: infer A5): infer R5; - (...args: infer A6): infer R6; - } - ? Fn | - Fn | - Fn | - Fn | - Fn | - Fn + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; + (...args: infer A6): infer R6; + (...args: infer A7): infer R7; + } + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; + (...args: A7): Promisify; + } : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; + (...args: infer A6): infer R6; + } + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + (...args: A6): Promisify; + } + + : T extends { + (...args: infer A0): infer R0; + (...args: infer A1): infer R1; + (...args: infer A2): infer R2; + (...args: infer A3): infer R3; + (...args: infer A4): infer R4; + (...args: infer A5): infer R5; + } + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + (...args: A5): Promisify; + } + + : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; (...args: infer A4): infer R4; - (...args: infer A5): infer R5; } - ? Fn | - Fn | - Fn | - Fn | - Fn + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + (...args: A4): Promisify; + } : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; (...args: infer A3): infer R3; - (...args: infer A4): infer R4; } - ? Fn | - Fn | - Fn | - Fn + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + (...args: A3): Promisify; + } : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; (...args: infer A2): infer R2; - (...args: infer A3): infer R3; } - ? Fn | - Fn | - Fn + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + (...args: A2): Promisify; + } : T extends { + (...args: infer A0): infer R0; (...args: infer A1): infer R1; - (...args: infer A2): infer R2; } - ? Fn | - Fn + ? { + (...args: A0): Promisify; + (...args: A1): Promisify; + } - : T extends (...args: infer A1) => infer R1 - ? Fn + : T extends (...args: infer A) => infer R + ? (...args: A) => Promisify : never; -type UnionToIntersection = - (Fn extends any ? (fn: Fn) => void : never) extends (fn: infer F) => void ? F : never; - +/** + * Adds `Promise` properties to the specified value + */ type WithPromise = Wrapped & Promise; +/** + * Maps primitive values to their object representation and "unwraps" `PromiseLike` objects + */ type GetSchema = Value extends string ? String : Value extends number @@ -83,20 +182,28 @@ type GetSchema = Value extends string ? Boolean : Value extends bigint ? BigInt + : Value extends symbol + ? Symbol : Value extends PromiseLike ? GetSchema - : Value extends any[] - ? WithPromise : Value; -type PromisifySchema = WithPromise< - { - [Key in keyof Schema]: Schema[Key] extends AnyFunction - ? UnionToIntersection> - : Promisify; - }, - Origin ->; - +/** + * Promisifies members of the specified schema by creating an object with the promisified properties + * or promisifying return type of each function overload + */ +type PromisifySchema = Schema extends (...args: any[]) => any + ? Overloads + + : WithPromise< + { + [Key in keyof Schema]: Promisify; + }, + Origin + >; + +/** + * Patches all members of the specified value in such a way that + * each of them will be wrapped in a promise but at the same time preserving its own properties + */ export type Promisify = PromisifySchema, Value>; - diff --git a/src/spec.ts b/src/spec.ts index 686ffd0..6e339ad 100644 --- a/src/spec.ts +++ b/src/spec.ts @@ -1,4 +1,5 @@ import { async } from './index'; +import { expectType } from './utils'; interface Data { user: { name: string[], age: number }; @@ -7,26 +8,29 @@ interface Data { const data: Data = { user: { name: ['foo'], age: 21 } }; describe('async', () => { - it('returns curried promise from non-promise value', async () => { - const twentyOne = await async(21); - expect(twentyOne).toBe(21); + it('Returns curried promise from non-promise value', async () => { + const value = await async(21); + expect(value).toBe(21); + expectType(); }); - it('returns curried promise from value wrapped in promise-like', async () => { + it('Returns curried promise from value wrapped in promise-like', async () => { const promise = Promise.resolve(data); - const name = await async(promise).user.name[0].toUpperCase().split(''); - expect(name).toEqual(['F', 'O', 'O']); + const value = await async(promise).user.name.at(0)?.toUpperCase().split(''); + expect(value).toEqual(['F', 'O', 'O']); + expectType(); }); - it('returns curried promise from function that returns promise-like', async () => { + it('Returns curried promise from function that returns promise-like', async () => { const getPromise = () => new Promise((resolve) => { setTimeout(resolve, 200, data); }); - const name = await async(getPromise).user.name.at(0); - expect(name).toBe('foo'); + const value = await async(getPromise).user.name.at(0); + expect(value).toBe('foo'); + expectType(); }); - it('creates independent promises', async () => { + it('Creates independent promises', async () => { const promise = Promise.resolve(data); const name = async(promise).user.name[0]; const upperName = name.toUpperCase(); @@ -34,18 +38,19 @@ describe('async', () => { const result = await Promise.all([name, upperName, arr]); expect(result).toEqual(['foo', 'FOO', ['F', 'O', 'O']]); + expectType<[string, string, string[]], typeof result>(); }); - it('unwraps promise-like objects so that it is possible to have a chain of these promise-likes', async () => { + it('Unwraps promise-like objects so that it is possible to have a chain of these promise-likes', async () => { const end = { - end() { - return { then(cb: (n: number) => void) { cb(21) } }; + end(): PromiseLike { + return { then(cb: (n: number) => any): any { cb(21); } }; } }; const process = { process() { - return new Promise(resolve => setTimeout(resolve, 1e3, end)); + return new Promise((resolve) => setTimeout(() => resolve(end), 1e3)); }, }; @@ -55,7 +60,8 @@ describe('async', () => { }, }; - const twentyOne = await async(chain).start().process().end(); - expect(twentyOne).toBe(21); + const value = await async(chain).start().process().end(); + expect(value).toBe(21); + expectType(); }); }); diff --git a/src/utils.ts b/src/utils.ts index cf43573..17d2294 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,3 +2,84 @@ export function cast(value: any): T { return value; } + +/** + * Checks if provided value is a type of function + * + * @param value + */ +export function isFunction(value: any): value is ((...args: any[]) => any) { + return typeof value === 'function'; +} + +/** + * The function implements the logic of chaining promises flatly using the `Proxy` object. + * It creates a chain of promises where each next promise takes a value from the previous one. + * + * @param getPrevPromiseLike - the function that returns the previous `PromiseLike` in chain + */ +export function proxymify(getPrevPromiseLike: (...args: unknown[]) => PromiseLike): unknown { + return new Proxy(getPrevPromiseLike, { + get(_: unknown, nextProp: string): unknown { + const prevPromiseLike = getPrevPromiseLike(); + return handleNativePromise(prevPromiseLike, nextProp) ?? proxymifyNextValue(prevPromiseLike, nextProp); + }, + + apply(target: (...args: unknown[]) => PromiseLike, _: unknown, args: unknown[]): unknown { + return proxymifyNextValueFromMethodCall(target, args); + } + }); +} + +/** + * Checks if the passed prop is in the `Promise.prototype` and tries to get value by this prop. + * + * @param prevPromiseLike - previous `PromiseLike` + * @param nextProp - possible key from `Promise.prototype` + */ +function handleNativePromise(prevPromiseLike: PromiseLike, nextProp: string | symbol): unknown { + if (!Object.hasOwn(Promise.prototype, nextProp)) { + return; + } + + const value = prevPromiseLike[cast>(nextProp)]; + return isFunction(value) ? value.bind(prevPromiseLike) : value; +} + +/** + * Creates next promise in chain that gets a value from the previous one by accessing it using the specified prop. + * + * @param prevPromiseLike - previous `PromiseLike` + * @param nextProp - key to get next value + */ +function proxymifyNextValue(prevPromiseLike: PromiseLike, nextProp: string | symbol): unknown { + return proxymify(async () => { + const data = await prevPromiseLike; + const value = cast>(data)[cast(nextProp)]; + return isFunction(value) ? value.bind(data) : value; + }); +} + +/** + * Creates next promise in chain that gets value from the previous one by calling the function. + * This function is called when we try to call a method on the previous proxied object. + * + * @param getMethod - the function that returns `PromiseLike` with currently calling method + * @param args - arguments for the method + */ +function proxymifyNextValueFromMethodCall(getMethod: () => PromiseLike, args: unknown[]): unknown { + return proxymify(async () => { + const method = await getMethod(); + return method(...args); + }); +} + +type Fn = (() => V extends T ? 1 : 0); + +type AreEquals = Fn extends Fn ? unknown : never; + +/** + * Util for checking two types equality + */ +// eslint-disable-next-line @typescript-eslint/no-empty-function +export function expectType>(): void {}