Repository: marpple/FxTS (ts)

Issue:
Support `thenable` object  to be used like `Promise`
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

<!-- What would you like to be added? -->

The thenable object is often also used like a promise object.

fxts is a functional library, and its combination with other libraries is important.

When returning an asynchronous function from another library, there are cases where it does not return a Promise object but returns it according to the Promsie spec.

Below is an example of prisma.

https://github.com/prisma/prisma/blob/3855438b964b40edeb72a3815c641d97f8c433c4/packages/client/src/runtime/core/request/createPrismaPromise.ts#L15-L55

```ts
export function createPrismaPromise(
  callback: (txId?: string | number, lock?: PromiseLike<void>, otelCtx?: Context) => PrismaPromise<unknown>,
): PrismaPromise<unknown> {
  ...
  return {
    then(onFulfilled, onRejected, txId?: string) {
      return _callback(txId).then(onFulfilled, onRejected, txId)
    },
    catch(onRejected, txId?: string) {
      return _callback(txId).catch(onRejected, txId)
    },
    finally(onFinally, txId?: string) {
      return _callback(txId).finally(onFinally, txId)
    },
   ...
```

So I think fxts should also support theanble object.



## 💻 Use Cases

<!-- What do you want to use this for? -->


Pull request description:
feat: support thenable
Fixes #163


Patch:
diff --git a/src/Lazy/append.ts b/src/Lazy/append.ts
index f9114b69..bd3e1db4 100644
--- a/src/Lazy/append.ts
+++ b/src/Lazy/append.ts
@@ -1,4 +1,4 @@
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 import concurrent, { isConcurrent } from "./concurrent";
 import Awaited from "../types/Awaited";
 import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
@@ -121,7 +121,7 @@ function append<A, B extends Iterable<A> | AsyncIterable<A>>(
   }
 
   if (isAsyncIterable(iterable)) {
-    return async(a instanceof Promise ? a : Promise.resolve(a), iterable);
+    return async(isPromise(a) ? a : Promise.resolve(a), iterable);
   }
 
   if (isIterable(iterable)) {
diff --git a/src/Lazy/dropUntil.ts b/src/Lazy/dropUntil.ts
index f5231ad3..91585453 100644
--- a/src/Lazy/dropUntil.ts
+++ b/src/Lazy/dropUntil.ts
@@ -1,7 +1,7 @@
 import IterableInfer from "../types/IterableInfer";
 import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
 import { AsyncFunctionException } from "../_internal/error";
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 import concurrent, { isConcurrent } from "./concurrent";
 
 function* sync<A, B>(f: (a: A) => B, iterable: Iterable<A>) {
@@ -14,7 +14,7 @@ function* sync<A, B>(f: (a: A) => B, iterable: Iterable<A>) {
 
   for (const a of iterableIterator) {
     const res = f(a);
-    if (res instanceof Promise) {
+    if (isPromise(res)) {
       throw new AsyncFunctionException();
     }
 
diff --git a/src/Lazy/dropWhile.ts b/src/Lazy/dropWhile.ts
index bf9b068f..e623d2d3 100644
--- a/src/Lazy/dropWhile.ts
+++ b/src/Lazy/dropWhile.ts
@@ -1,7 +1,7 @@
 import IterableInfer from "../types/IterableInfer";
 import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
 import { AsyncFunctionException } from "../_internal/error";
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 import concurrent, { isConcurrent } from "./concurrent";
 
 function* sync<A, B>(f: (a: A) => B, iterable: Iterable<A>) {
@@ -14,7 +14,7 @@ function* sync<A, B>(f: (a: A) => B, iterable: Iterable<A>) {
 
   for (const a of iterableIterator) {
     const res = f(a);
-    if (res instanceof Promise) {
+    if (isPromise(res)) {
       throw new AsyncFunctionException();
     }
 
diff --git a/src/Lazy/filter.ts b/src/Lazy/filter.ts
index d5bd1609..f4a920f8 100644
--- a/src/Lazy/filter.ts
+++ b/src/Lazy/filter.ts
@@ -1,4 +1,4 @@
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 import concurrent, { Concurrent, isConcurrent } from "./concurrent";
 import pipe1 from "../pipe1";
 import IterableInfer from "../types/IterableInfer";
@@ -155,7 +155,7 @@ function* sync<A>(f: (a: A) => unknown, iterable: Iterable<A>) {
   for (const a of iterable) {
     const res = f(a);
 
-    if (res instanceof Promise) {
+    if (isPromise(res)) {
       throw new AsyncFunctionException();
     }
 
diff --git a/src/Lazy/map.ts b/src/Lazy/map.ts
index 2c657a3e..846dc76a 100644
--- a/src/Lazy/map.ts
+++ b/src/Lazy/map.ts
@@ -1,4 +1,4 @@
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 import Awaited from "../types/Awaited";
 import IterableInfer from "../types/IterableInfer";
 import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
@@ -21,7 +21,7 @@ function sync<A, B>(
       }
 
       const res = f(value);
-      if (res instanceof Promise) {
+      if (isPromise(res)) {
         throw new AsyncFunctionException();
       }
 
diff --git a/src/Lazy/prepend.ts b/src/Lazy/prepend.ts
index e161e15d..cff5abd5 100644
--- a/src/Lazy/prepend.ts
+++ b/src/Lazy/prepend.ts
@@ -1,6 +1,6 @@
 import Awaited from "../types/Awaited";
 import ReturnPrependType from "../types/ReturnPrependType";
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 
 function* sync<A>(a: A, iterable: Iterable<A>): IterableIterator<A> {
   yield a;
@@ -90,7 +90,7 @@ function prepend<A, B extends Iterable<A> | AsyncIterable<Awaited<A>>>(
   }
 
   if (isAsyncIterable(iterable)) {
-    return async(a instanceof Promise ? a : Promise.resolve(a), iterable);
+    return async(isPromise(a) ? a : Promise.resolve(a), iterable);
   }
 
   if (isIterable(iterable)) {
diff --git a/src/Lazy/takeUntil.ts b/src/Lazy/takeUntil.ts
index 19d37ff7..5d50e99e 100644
--- a/src/Lazy/takeUntil.ts
+++ b/src/Lazy/takeUntil.ts
@@ -1,4 +1,4 @@
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 import IterableInfer from "../types/IterableInfer";
 import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
 import { AsyncFunctionException } from "../_internal/error";
@@ -9,7 +9,7 @@ function* sync<A, B>(f: (a: A) => B, iterable: Iterable<A>) {
     yield item;
 
     const res = f(item);
-    if (res instanceof Promise) {
+    if (isPromise(res)) {
       throw new AsyncFunctionException();
     }
 
diff --git a/src/Lazy/takeWhile.ts b/src/Lazy/takeWhile.ts
index b8358013..bcd1f9e4 100644
--- a/src/Lazy/takeWhile.ts
+++ b/src/Lazy/takeWhile.ts
@@ -1,4 +1,4 @@
-import { isAsyncIterable, isIterable } from "../_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "../_internal/utils";
 import IterableInfer from "../types/IterableInfer";
 import ReturnIterableIteratorType from "../types/ReturnIterableIteratorType";
 import { AsyncFunctionException } from "../_internal/error";
@@ -7,7 +7,7 @@ import concurrent, { isConcurrent } from "./concurrent";
 function* sync<A, B>(f: (a: A) => B, iterable: Iterable<A>) {
   for (const item of iterable) {
     const res = f(item);
-    if (res instanceof Promise) {
+    if (isPromise(res)) {
       throw new AsyncFunctionException();
     }
 
diff --git a/src/Lazy/toAsync.ts b/src/Lazy/toAsync.ts
index c13dd5b9..1819f286 100644
--- a/src/Lazy/toAsync.ts
+++ b/src/Lazy/toAsync.ts
@@ -1,3 +1,5 @@
+import { isPromise } from "../_internal/utils";
+
 /**
  * Returns AsyncIterable, `toAsync` used when you want to handle Promise values inside Iterable.
  *
@@ -28,7 +30,7 @@ function toAsync<T>(iter: Iterable<T | Promise<T>>): AsyncIterableIterator<T> {
   return {
     async next() {
       const { value, done } = iterator.next();
-      if (value instanceof Promise) {
+      if (isPromise(value)) {
         return value.then((value) => ({ done, value }));
       } else {
         return { done, value };
diff --git a/src/_internal/utils.ts b/src/_internal/utils.ts
index e48975ff..d88fb8e6 100644
--- a/src/_internal/utils.ts
+++ b/src/_internal/utils.ts
@@ -40,3 +40,20 @@ export const asyncEmpty = async function* () {};
 
 export const isNotNullable = <T>(a: T): a is NonNullable<T> =>
   a !== null && a !== undefined;
+
+export const isPromise = <T>(a: Promise<T> | T): a is Promise<T> => {
+  if (a instanceof Promise) {
+    return true;
+  }
+
+  if (
+    a !== null &&
+    typeof a === "object" &&
+    typeof (a as any).then === "function" &&
+    typeof (a as any).catch === "function"
+  ) {
+    return true;
+  }
+
+  return false;
+};
diff --git a/src/add.ts b/src/add.ts
index e0e280cc..8728fba4 100644
--- a/src/add.ts
+++ b/src/add.ts
@@ -1,6 +1,7 @@
 import isNumber from "./isNumber";
 import isString from "./isString";
 import Awaited from "./types/Awaited";
+import { isPromise } from "./_internal/utils";
 
 // prettier-ignore
 type ReturnAddType<T, A extends T, B extends T> = 
@@ -71,7 +72,7 @@ function add<
     };
   }
 
-  if (a instanceof Promise || b instanceof Promise) {
+  if (isPromise(a) || isPromise(b)) {
     return async(Promise.resolve(a), Promise.resolve(b));
   }
 
diff --git a/src/countBy.ts b/src/countBy.ts
index b54335f8..119f515d 100644
--- a/src/countBy.ts
+++ b/src/countBy.ts
@@ -1,7 +1,7 @@
 import Key from "./types/Key";
 import ReturnValueType from "./types/ReturnValueType";
 import IterableInfer from "./types/IterableInfer";
-import { isAsyncIterable, isIterable } from "./_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "./_internal/utils";
 import { AsyncFunctionException } from "./_internal/error";
 import reduce from "./reduce";
 
@@ -78,7 +78,7 @@ function countBy<
     return reduce(
       (group, a) => {
         const key = f(a);
-        if (key instanceof Promise) {
+        if (isPromise(key)) {
           throw new AsyncFunctionException();
         }
         return incSel(group, key);
diff --git a/src/delay.ts b/src/delay.ts
index a5c59eaf..8ff62f94 100644
--- a/src/delay.ts
+++ b/src/delay.ts
@@ -1,3 +1,5 @@
+import { isPromise } from "./_internal/utils";
+
 /**
  * Delays the value by given `wait` time
  *
@@ -11,7 +13,7 @@ function delay(wait: number): Promise<void>;
 function delay<T>(wait: number, value: T): Promise<T>;
 function delay<T>(wait: number, value?: T): Promise<T | undefined> {
   return new Promise((resolve, reject) => {
-    if (value instanceof Promise) {
+    if (isPromise(value)) {
       value.catch(reject);
     }
 
diff --git a/src/each.ts b/src/each.ts
index 03e24244..63d72a89 100644
--- a/src/each.ts
+++ b/src/each.ts
@@ -1,6 +1,6 @@
 import IterableInfer from "./types/IterableInfer";
 import ReturnValueType from "./types/ReturnValueType";
-import { isAsyncIterable, isIterable } from "./_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "./_internal/utils";
 
 function sync<A, B = unknown>(f: (a: A) => B, iterable: Iterable<A>): void {
   for (const a of iterable) {
@@ -14,7 +14,7 @@ async function async<A, B = unknown>(
 ): Promise<void> {
   for await (const item of iterable) {
     const value = f(item);
-    if (value instanceof Promise) {
+    if (isPromise(value)) {
       await value;
     }
   }
diff --git a/src/groupBy.ts b/src/groupBy.ts
index 62c19ef3..331087f2 100644
--- a/src/groupBy.ts
+++ b/src/groupBy.ts
@@ -1,7 +1,7 @@
 import Key from "./types/Key";
 import ReturnValueType from "./types/ReturnValueType";
 import IterableInfer from "./types/IterableInfer";
-import { isAsyncIterable, isIterable } from "./_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "./_internal/utils";
 import { AsyncFunctionException } from "./_internal/error";
 import reduce from "./reduce";
 import iterableInfer from "./types/IterableInfer";
@@ -79,7 +79,7 @@ function groupBy<
     return reduce(
       (group, a) => {
         const key = f(a);
-        if (key instanceof Promise) {
+        if (isPromise(key)) {
           throw new AsyncFunctionException();
         }
         return (group[key] || (group[key] = [])).push(a), group;
diff --git a/src/indexBy.ts b/src/indexBy.ts
index ac1e29f5..bbd09d6e 100644
--- a/src/indexBy.ts
+++ b/src/indexBy.ts
@@ -1,7 +1,7 @@
 import Key from "./types/Key";
 import ReturnValueType from "./types/ReturnValueType";
 import IterableInfer from "./types/IterableInfer";
-import { isAsyncIterable, isIterable } from "./_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "./_internal/utils";
 import reduce from "./reduce";
 import { AsyncFunctionException } from "./_internal/error";
 
@@ -71,7 +71,7 @@ function indexBy<
     return reduce(
       (group, a) => {
         const key = f(a);
-        if (key instanceof Promise) {
+        if (isPromise(key)) {
           throw new AsyncFunctionException();
         }
         return (group[key] = a), group;
diff --git a/src/omitBy.ts b/src/omitBy.ts
index 118b7949..42dd6c52 100644
--- a/src/omitBy.ts
+++ b/src/omitBy.ts
@@ -9,6 +9,7 @@ import toAsync from "./Lazy/toAsync";
 import map from "./Lazy/map";
 import zip from "./Lazy/zip";
 import pipe from "./pipe";
+import { isPromise } from "./_internal/utils";
 
 /**
  *
@@ -78,7 +79,7 @@ function omitBy<T extends object, F extends ConditionalAsyncEntryPredicate<T>>(
 
   const entries = Object.entries(obj);
   const conditions = entries.map((entry) => f(entry as any));
-  const isAsync = conditions.some((c) => c instanceof Promise);
+  const isAsync = conditions.some((c) => isPromise(c));
 
   if (isAsync) {
     return pipe(
diff --git a/src/partition.ts b/src/partition.ts
index 3e1fba00..3236ea83 100644
--- a/src/partition.ts
+++ b/src/partition.ts
@@ -1,6 +1,6 @@
 import IterableInfer from "./types/IterableInfer";
 import ReturnPartitionType from "./types/ReturnPartitionType";
-import { isAsyncIterable, isIterable } from "./_internal/utils";
+import { isAsyncIterable, isIterable, isPromise } from "./_internal/utils";
 import groupBy from "./groupBy";
 import { AsyncFunctionException } from "./_internal/error";
 
@@ -69,7 +69,7 @@ function partition<A extends Iterable<unknown> | AsyncIterable<unknown>, B>(
   if (isIterable<IterableInfer<A>>(iterable)) {
     const group = groupBy((a) => {
       const key = f(a);
-      if (key instanceof Promise) {
+      if (isPromise(key)) {
         throw new AsyncFunctionException();
       }
       return `${Boolean(key)}`;
diff --git a/src/pickBy.ts b/src/pickBy.ts
index c48a2085..d93fef66 100644
--- a/src/pickBy.ts
+++ b/src/pickBy.ts
@@ -9,6 +9,7 @@ import toAsync from "./Lazy/toAsync";
 import map from "./Lazy/map";
 import zip from "./Lazy/zip";
 import pipe from "./pipe";
+import { isPromise } from "./_internal/utils";
 
 /**
  *
@@ -78,7 +79,7 @@ function pickBy<T extends object, F extends ConditionalAsyncEntryPredicate<T>>(
 
   const entries = Object.entries(obj);
   const conditions = entries.map((entry) => f(entry as any));
-  const isAsync = conditions.some((c) => c instanceof Promise);
+  const isAsync = conditions.some((c) => isPromise(c));
 
   if (isAsync) {
     return pipe(
diff --git a/src/pipe1.ts b/src/pipe1.ts
index 4d3c5600..07b8e399 100644
--- a/src/pipe1.ts
+++ b/src/pipe1.ts
@@ -1,4 +1,5 @@
 import Awaited from "./types/Awaited";
+import { isPromise } from "./_internal/utils";
 
 type Pipe1 = <P, F extends (a: P extends Promise<any> ? Awaited<P> : P) => any>(
   a: P,
@@ -13,7 +14,7 @@ type Pipe1 = <P, F extends (a: P extends Promise<any> ? Awaited<P> : P) => any>(
  * @internal
  */
 const pipe1: Pipe1 = (a, f) => {
-  return a instanceof Promise ? a.then(f) : f(a as any);
+  return isPromise(a) ? a.then(f as any) : f(a as any);
 };
 
 export default pipe1;
diff --git a/src/tap.ts b/src/tap.ts
index 9c063e2e..a4c108f7 100644
--- a/src/tap.ts
+++ b/src/tap.ts
@@ -1,4 +1,5 @@
 import Awaited from "./types/Awaited";
+import { isPromise } from "./_internal/utils";
 /**
  * This method invokes interceptor and returns a value.
  * The interceptor is invoked with one argument.
@@ -34,8 +35,8 @@ function tap<T, U>(
     return (v: T) => tap(f, v);
   }
 
-  const res = v instanceof Promise ? v.then(f) : f(v as Awaited<T>);
-  if (res instanceof Promise) {
+  const res = isPromise(v) ? v.then(f as any) : (f(v as Awaited<T>) as any);
+  if (isPromise(res)) {
     return res.then(() => v);
   }
 


Test patch:
diff --git a/test/pipe.spec.ts b/test/pipe.spec.ts
index bfb5f25b..24631a87 100644
--- a/test/pipe.spec.ts
+++ b/test/pipe.spec.ts
@@ -1,4 +1,6 @@
 import {
+  delay,
+  each,
   filter,
   map,
   pipe,
@@ -74,5 +76,24 @@ describe("pipe", function () {
         ),
       ).rejects.toThrow("err");
     });
+
+    it("should be able to use thenable object", async function () {
+      let res = 0;
+
+      await pipe(
+        ["a", "b", "c"],
+        toAsync,
+        each(() => {
+          return {
+            then: (resolve: any) => {
+              resolve(delay(0).then(() => res++));
+            },
+            catch: (reject: any) => reject(),
+          };
+        }),
+      );
+
+      expect(res).toBe(3);
+    });
   });
 });