Repository: marpple/FxTS
Pull request #284 (2024-10-08 07:50:00)

refactor: typeguard 타입추론 개선
Fixes #
기존 타입가드 함수의 타입 추론 결과의 정확도를 향상시켰습니다.
최신 TS 버전은 타입가드의 결과를 input 타입과 교집합으로 계삽합니다. 이를 참고하여 타입 연산식을 수정했습니다.

아쉽게도 여전히 input이 any인 경우는 잘못된 결과가 추론됩니다.

before
```ts
isString(input: any); // input is any;
isString(input: unknown); // input is never;
isString(input: number | "1" | "2"); // input is "1" | "2";
```

after
```ts
isString(input: any); // input is any;
isString(input: unknown); // input is string;
isString(input: number | "1" | "2"); // input is "1" | "2";
isString(input: number); // input is never;
```

rojiwon123: is 함수의 인자인 input을 unknown으로 정의하면 any를 포함한 타입추론이 꽤 잘 이루어집니다. 하지만 unless 같은 다른 유틸 함수들과 함께 사용할 경우, 오히려 기존 인자의 타입을 상실하게 되기에 아직도 개선의 여지가 남아있습니다.
ppeeou: 
@rojiwon123  Thank you for your interest and contribution.

It seems that the PR example enhances the type inference for `isString`, but I don't see that in the code. Could you also include the details about how the type inference for `isString` has been improved?

Patch:
diff --git a/package-lock.json b/package-lock.json
index fd746beb..a8987635 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
 {
   "name": "@fxts/core",
-  "version": "1.0.1",
+  "version": "1.1.0",
   "lockfileVersion": 3,
   "requires": true,
   "packages": {
     "": {
       "name": "@fxts/core",
-      "version": "1.0.1",
+      "version": "1.1.0",
       "license": "Apache-2.0",
       "dependencies": {
         "tslib": "^2.6.0"
diff --git a/package.json b/package.json
index fa39d27a..af8b1283 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
 {
   "name": "@fxts/core",
-  "version": "1.0.1",
+  "version": "1.1.0",
   "description": "A functional library for TypeScript/JavaScript programmers.",
   "main": "dist/cjs/index.js",
   "module": "dist/esm/index.js",
diff --git a/src/Lazy/differenceBy.ts b/src/Lazy/differenceBy.ts
index da87fe61..58a26dac 100644
--- a/src/Lazy/differenceBy.ts
+++ b/src/Lazy/differenceBy.ts
@@ -1,3 +1,4 @@
+import { throwIfPromiseError } from "../_internal/error";
 import { isAsyncIterable, isIterable } from "../_internal/utils";
 import pipe from "../pipe";
 import pipe1 from "../pipe1";
@@ -13,7 +14,7 @@ function* sync<T>(
   iterable1: Iterable<T>,
   iterable2: Iterable<T>,
 ) {
-  const set = new Set(map(f, iterable1));
+  const set = new Set(map((a) => throwIfPromiseError(f(a)), iterable1));
 
   yield* pipe(
     iterable2,
diff --git a/src/Lazy/dropRight.ts b/src/Lazy/dropRight.ts
index 92d9695b..618af9a1 100644
--- a/src/Lazy/dropRight.ts
+++ b/src/Lazy/dropRight.ts
@@ -9,7 +9,7 @@ function* sync<T>(length: number, iterable: Iterable<T>) {
   const arr =
     isArray(iterable) || isString(iterable) ? iterable : toArray(iterable);
   for (let i = 0; i < arr.length - length; i++) {
-    yield arr[i];
+    yield arr[i] as T;
   }
 }
 
diff --git a/src/Lazy/flatMap.ts b/src/Lazy/flatMap.ts
index c3000b26..1b1e3e34 100644
--- a/src/Lazy/flatMap.ts
+++ b/src/Lazy/flatMap.ts
@@ -1,3 +1,4 @@
+import { throwIfPromiseError } from "../_internal/error";
 import { isAsyncIterable, isIterable } from "../_internal/utils";
 import type Awaited from "../types/Awaited";
 import type { DeepFlat, DeepFlatSync } from "../types/DeepFlat";
@@ -87,12 +88,12 @@ function flatMap<
 ): ReturnFlatMapType<A, B> | ((iterable: A) => ReturnFlatMapType<A, B>) {
   if (iterable === undefined) {
     return (iterable: A) => {
-      return flat(map(f, iterable as any)) as any;
+      return flatMap(f, iterable) as any;
     };
   }
 
   if (isIterable<IterableInfer<A>>(iterable)) {
-    return flat(map(f, iterable as any)) as any;
+    return flat(map((a) => throwIfPromiseError(f(a)), iterable)) as any;
   }
 
   if (isAsyncIterable<Awaited<IterableInfer<A>>>(iterable)) {
diff --git a/src/Lazy/fx.ts b/src/Lazy/fx.ts
index 7ee8e164..6265c128 100644
--- a/src/Lazy/fx.ts
+++ b/src/Lazy/fx.ts
@@ -40,6 +40,22 @@ class FxAsyncIterable<A> {
     return this.asyncIterable[Symbol.asyncIterator]();
   }
 
+  /**
+   * It takes a user-defined function that transforms the current object and returns the result of that transformation.
+   *
+   * @example
+   * ```ts
+   * const arrSize = await fx([5, 2, 3, 1, 4, 5, 3])
+   *   .toAsync()
+   *   .filter((n) => n % 2 === 1)
+   *   .map((n) => n * 10)
+   *   .to((iterable) => size(uniq(iterable)));
+   * ```
+   */
+  to<R>(converter: (asyncIterable: this) => R): R {
+    return converter(this);
+  }
+
   /**
    * Returns AsyncIterable of values by running each applying `f`.
    *
@@ -269,6 +285,7 @@ class FxAsyncIterable<A> {
 
   /**
    * Iterates over AsyncIterable, applying each in turn to `f`.
+   * It works the same way as `forEach`.
    *
    * see {@link https://fxts.dev/docs/each | each}
    */
@@ -276,6 +293,16 @@ class FxAsyncIterable<A> {
     return each(f, this.asyncIterable);
   }
 
+  /**
+   * Iterates over AsyncIterable, applying each in turn to `f`.
+   * It works the same way as `each`.
+   *
+   * see {@link https://fxts.dev/docs/each | each}
+   */
+  async forEach(f: (a: A) => unknown): Promise<void> {
+    return each(f, this.asyncIterable);
+  }
+
   /**
    * Takes item from AsyncIterable and returns an array.
    *
@@ -301,6 +328,24 @@ export class FxIterable<A> {
     return this.iterable[Symbol.iterator]();
   }
 
+  /**
+   * It takes a user-defined function that transforms the current object and returns the result of that transformation.
+   *
+   *
+   * @example
+   * ```ts
+   * const size = fx([5, 2, 3, 1, 4, 5, 3])
+   *  .filter(n => n % 2 === 1)
+   *  .map(n => n * 10)
+   *  .to(iterable => new Set(iterable)) // convert set
+   *  .add(10)
+   *  .size;
+   * ```
+   */
+  to<R>(converter: (iterable: this) => R): R {
+    return converter(this);
+  }
+
   /**
    * Returns Iterable of values by running each applying `f`.
    *
@@ -516,6 +561,7 @@ export class FxIterable<A> {
 
   /**
    * Iterates over Iterable, applying each in turn to `f`.
+   * It works the same way as `forEach`.
    *
    * see {@link https://fxts.dev/docs/each | each}
    */
@@ -523,6 +569,16 @@ export class FxIterable<A> {
     return each(f, this.iterable);
   }
 
+  /**
+   * Iterates over Iterable, applying each in turn to `f`.
+   * It works the same way as `each`.
+   *
+   * see {@link https://fxts.dev/docs/each | each}
+   */
+  forEach(f: (a: A) => unknown): void {
+    return each(f, this.iterable);
+  }
+
   /**
    * Takes item from Iterable and returns an array.
    *
diff --git a/src/Lazy/intersectionBy.ts b/src/Lazy/intersectionBy.ts
index 91f76b24..1275dbb7 100644
--- a/src/Lazy/intersectionBy.ts
+++ b/src/Lazy/intersectionBy.ts
@@ -1,3 +1,4 @@
+import { throwIfPromiseError } from "../_internal/error";
 import { isAsyncIterable, isIterable } from "../_internal/utils";
 import pipe from "../pipe";
 import pipe1 from "../pipe1";
@@ -13,7 +14,7 @@ function* sync<T>(
   iterable1: Iterable<T>,
   iterable2: Iterable<T>,
 ) {
-  const set = new Set(map(f, iterable1));
+  const set = new Set(map((a) => throwIfPromiseError(f(a)), iterable1));
 
   yield* pipe(
     iterable2,
diff --git a/src/Lazy/map.ts b/src/Lazy/map.ts
index 9cf2cc14..30749182 100644
--- a/src/Lazy/map.ts
+++ b/src/Lazy/map.ts
@@ -20,14 +20,9 @@ function sync<A, B>(
         };
       }
 
-      const res = f(value);
-      if (isPromise(res)) {
-        throw new AsyncFunctionException();
-      }
-
       return {
         done: false,
-        value: res,
+        value: f(value),
       };
     },
     [Symbol.iterator]() {
diff --git a/src/Lazy/takeRight.ts b/src/Lazy/takeRight.ts
index a96d4bc2..0841275d 100644
--- a/src/Lazy/takeRight.ts
+++ b/src/Lazy/takeRight.ts
@@ -11,7 +11,7 @@ function* sync<A>(length: number, iterable: Iterable<A>): IterableIterator<A> {
     isArray(iterable) || isString(iterable) ? iterable : toArray(iterable);
   const index = arr.length - length;
   for (let i = index; i < arr.length; i++) {
-    if (arr[i]) yield arr[i];
+    if (arr[i]) yield arr[i] as A;
   }
 }
 
diff --git a/src/Lazy/zipWith.ts b/src/Lazy/zipWith.ts
index 02658e10..5b770d72 100644
--- a/src/Lazy/zipWith.ts
+++ b/src/Lazy/zipWith.ts
@@ -1,3 +1,4 @@
+import { throwIfPromiseError } from "../_internal/error";
 import { isAsyncIterable, isIterable } from "../_internal/utils";
 import map from "./map";
 import zip from "./zip";
@@ -44,7 +45,10 @@ function zipWith<A, B, C>(
   iterable2: Iterable<B> | AsyncIterable<B>,
 ): IterableIterator<C> | AsyncIterableIterator<C> {
   if (isIterable(iterable1) && isIterable(iterable2)) {
-    return map(([a, b]) => f(a, b), zip(iterable1, iterable2));
+    return map(
+      ([a, b]) => throwIfPromiseError(f(a, b)),
+      zip(iterable1, iterable2),
+    );
   }
   if (isIterable(iterable1) && isAsyncIterable(iterable2)) {
     return map(([a, b]) => f(a, b), zip(iterable1, iterable2));
diff --git a/src/_internal/error.ts b/src/_internal/error.ts
index f93d63ac..10b3f820 100644
--- a/src/_internal/error.ts
+++ b/src/_internal/error.ts
@@ -1,3 +1,5 @@
+import { isPromise } from "./utils";
+
 export class AsyncFunctionException extends Error {
   static MESSAGE = `'Iterable' can not used with async function.
 If you want to deal with async function, see: [toAsync](https://fxts.dev/docs/toAsync)`;
@@ -6,3 +8,11 @@ If you want to deal with async function, see: [toAsync](https://fxts.dev/docs/to
     super(message);
   }
 }
+
+export const throwIfPromiseError = <T>(a: Promise<T> | T): T => {
+  if (isPromise(a)) {
+    throw new AsyncFunctionException();
+  }
+
+  return a;
+};
diff --git a/src/every.ts b/src/every.ts
index 32d645b6..6d5859c9 100644
--- a/src/every.ts
+++ b/src/every.ts
@@ -1,3 +1,4 @@
+import { throwIfPromiseError } from "./_internal/error";
 import { isAsyncIterable, isIterable } from "./_internal/utils";
 import map from "./Lazy/map";
 import takeUntil from "./Lazy/takeUntil";
@@ -56,7 +57,7 @@ function every<
 
   if (isIterable<IterableInfer<A>>(iterable)) {
     return pipe(
-      map(f, iterable),
+      map((a) => throwIfPromiseError(f(a)), iterable),
       takeUntil(not),
       (acc) =>
         reduce(
diff --git a/src/isArray.ts b/src/isArray.ts
index 67a34607..af728e4f 100644
--- a/src/isArray.ts
+++ b/src/isArray.ts
@@ -1,5 +1,3 @@
-import type Include from "./types/Include";
-
 /**
  * Returns true if `a` is an Array.
  *
@@ -9,7 +7,7 @@ import type Include from "./types/Include";
  * isArray(2); // false
  * ```
  */
-const isArray = <T>(a: T): a is Include<T, unknown[] | Readonly<unknown[]>> =>
-  Array.isArray(a);
+const isArray = <T>(input: T): input is T & (unknown[] | Readonly<unknown[]>) =>
+  Array.isArray(input);
 
 export default isArray;
diff --git a/src/isBoolean.ts b/src/isBoolean.ts
index 4372fabc..ea46e7a6 100644
--- a/src/isBoolean.ts
+++ b/src/isBoolean.ts
@@ -1,5 +1,3 @@
-import type Include from "./types/Include";
-
 /**
  * Returns true if `n` is a Boolean.
  *
@@ -10,6 +8,7 @@ import type Include from "./types/Include";
  * isBoolean("FxTS"); // false
  * ```
  */
-const isBoolean = <T>(n: T): n is Include<T, boolean> => typeof n === "boolean";
+const isBoolean = <T>(input: T): input is T & boolean =>
+  typeof input === "boolean";
 
 export default isBoolean;
diff --git a/src/isNil.ts b/src/isNil.ts
index 05e0aa03..bb633a52 100644
--- a/src/isNil.ts
+++ b/src/isNil.ts
@@ -1,6 +1,5 @@
 import isNull from "./isNull";
 import isUndefined from "./isUndefined";
-import type Include from "./types/Include";
 
 /**
  * Checks if the given value is `null` or `undefined`.
@@ -13,7 +12,7 @@ import type Include from "./types/Include";
  * isNil(null); // true
  * ```
  */
-const isNil = <T>(a: T): a is Include<T, null | undefined> =>
-  isUndefined(a) || isNull(a);
+const isNil = <T>(input: T): input is T & (null | undefined) =>
+  isUndefined(input) || isNull(input);
 
 export default isNil;
diff --git a/src/isNull.ts b/src/isNull.ts
index 39dbc14e..1f1e1784 100644
--- a/src/isNull.ts
+++ b/src/isNull.ts
@@ -1,5 +1,3 @@
-import type Include from "./types/Include";
-
 /**
  * Checks if the given value is `null`.
  *
@@ -11,6 +9,6 @@ import type Include from "./types/Include";
  * isNull(null); // true
  * ```
  */
-const isNull = <T>(input: T): input is Include<T, null> => input === null;
+const isNull = <T>(input: T): input is T & null => input === null;
 
 export default isNull;
diff --git a/src/isNumber.ts b/src/isNumber.ts
index 73ce9a93..947186d5 100644
--- a/src/isNumber.ts
+++ b/src/isNumber.ts
@@ -1,5 +1,3 @@
-import type Include from "./types/Include";
-
 /**
  * Returns true if `n` is a Number.
  *
@@ -9,6 +7,7 @@ import type Include from "./types/Include";
  * isNumber("a"); // false
  * ```
  */
-const isNumber = <T>(n: T): n is Include<T, number> => typeof n === "number";
+const isNumber = <T>(input: T): input is T & number =>
+  typeof input === "number";
 
 export default isNumber;
diff --git a/src/isObject.ts b/src/isObject.ts
index 3b5f0aed..b2b2252b 100644
--- a/src/isObject.ts
+++ b/src/isObject.ts
@@ -1,4 +1,4 @@
-import type Include from "./types/Include";
+import isNil from "./isNil";
 
 /**
  * Checks if value is the type of object.
@@ -12,9 +12,9 @@ import type Include from "./types/Include";
  * isObject(123); // false
  * ```
  */
-const isObject = <T>(a: T): a is Include<T, object> => {
-  const type = typeof a;
-  return a != null && (type === "object" || type === "function");
+const isObject = <T>(input: T): input is T & object => {
+  const type = typeof input;
+  return !isNil(input) && (type === "object" || type === "function");
 };
 
 export default isObject;
diff --git a/src/isString.ts b/src/isString.ts
index 90362359..942d10ab 100644
--- a/src/isString.ts
+++ b/src/isString.ts
@@ -1,5 +1,3 @@
-import type Include from "./types/Include";
-
 /**
  * Returns true if `s` is a String.
  *
@@ -9,6 +7,7 @@ import type Include from "./types/Include";
  * isString(2); // false
  * ```
  */
-const isString = <T>(s: T): s is Include<T, string> => typeof s === "string";
+const isString = <T>(input: T): input is T & string =>
+  typeof input === "string";
 
 export default isString;
diff --git a/src/isUndefined.ts b/src/isUndefined.ts
index 2a3d63eb..e5d67ed6 100644
--- a/src/isUndefined.ts
+++ b/src/isUndefined.ts
@@ -1,5 +1,3 @@
-import type Include from "./types/Include";
-
 /**
  * Checks if the given value is `undefined`.
  *
@@ -9,6 +7,7 @@ import type Include from "./types/Include";
  * isUndefined(2); // false
  * ```
  */
-const isUndefined = <T>(a: T): a is Include<T, undefined> => a === undefined;
+const isUndefined = <T>(input: T): input is T & undefined =>
+  input === undefined;
 
 export default isUndefined;
diff --git a/src/some.ts b/src/some.ts
index c216db9c..46dfc016 100644
--- a/src/some.ts
+++ b/src/some.ts
@@ -1,3 +1,4 @@
+import { throwIfPromiseError } from "./_internal/error";
 import { isAsyncIterable, isIterable } from "./_internal/utils";
 import identity from "./identity";
 import map from "./Lazy/map";
@@ -76,7 +77,7 @@ function some<
 
   if (isIterable<IterableInfer<A>>(iterable)) {
     return pipe(
-      map(f, iterable),
+      map((a) => throwIfPromiseError(f(a)), iterable),
       takeUntil(identity),
       (acc) =>
         reduce(
diff --git a/src/types/FalsyTypeOf.ts b/src/types/FalsyTypeOf.ts
new file mode 100644
index 00000000..e8082a73
--- /dev/null
+++ b/src/types/FalsyTypeOf.ts
@@ -0,0 +1,5 @@
+import type Falsy from "./Falsy";
+
+type FalsyTypesOf<T> = T extends Falsy ? T : never;
+
+export default FalsyTypesOf;
diff --git a/src/types/Include.ts b/src/types/Include.ts
deleted file mode 100644
index bc9c23d8..00000000
--- a/src/types/Include.ts
+++ /dev/null
@@ -1,3 +0,0 @@
-type Include<T, N> = T extends N ? T : never;
-
-export default Include;


Test patch:
diff --git a/test/Lazy/fx.spec.ts b/test/Lazy/fx.spec.ts
index 45348c31..ef817d7e 100644
--- a/test/Lazy/fx.spec.ts
+++ b/test/Lazy/fx.spec.ts
@@ -1,4 +1,4 @@
-import { fx, toArray, toAsync } from "../../src";
+import { fx, size, toArray, toAsync, uniq } from "../../src";
 
 describe("fx", function () {
   describe("sync", function () {
@@ -16,6 +16,15 @@ describe("fx", function () {
         .toArray();
       expect(res).toEqual([11, 12, 13, 14, 15]);
     });
+
+    it("`to` method", function () {
+      const arrSize = fx([5, 2, 3, 1, 4, 5, 3])
+        .filter((n) => n % 2 === 1)
+        .map((n) => n * 10)
+        .to((iterable) => new Set(iterable))
+        .add(10).size;
+      expect(arrSize).toEqual(3);
+    });
   });
 
   describe("async", () => {
@@ -39,5 +48,15 @@ describe("fx", function () {
         .toArray();
       expect(res2).toEqual([11, 12, 13, 14, 15]);
     });
+
+    it("`to` method", async function () {
+      const arrSize = await fx([5, 2, 3, 1, 4, 5, 3])
+        .toAsync()
+        .filter((n) => n % 2 === 1)
+        .map((n) => n * 10)
+        .to((iterable) => size(uniq(iterable)));
+
+      expect(arrSize).toEqual(3);
+    });
   });
 });
diff --git a/test/Lazy/map.spec.ts b/test/Lazy/map.spec.ts
index f2ae9f19..eba7d636 100644
--- a/test/Lazy/map.spec.ts
+++ b/test/Lazy/map.spec.ts
@@ -1,4 +1,3 @@
-import { AsyncFunctionException } from "../../src/_internal/error";
 import { fx, map, pipe, range, toArray, toAsync } from "../../src/index";
 import { Concurrent } from "../../src/Lazy/concurrent";
 import { generatorMock } from "../utils";
@@ -16,11 +15,6 @@ describe("map", function () {
       expect(acc).toEqual([11, 12, 13, 14, 15]);
     });
 
-    it("should throw an error when the callback is asynchronous", function () {
-      const res = () => [...map(add10Async, [1, 2, 3, 4, 5])];
-      expect(res).toThrowError(new AsyncFunctionException());
-    });
-
     it("should be able to be used as a curried function in the pipeline", function () {
       const res = pipe(
         [1, 2, 3, 4],
@@ -92,6 +86,18 @@ describe("map", function () {
 
       expect(res).toEqual(["1", "2", "3", "4"]);
     });
+    it("should be able to be used as a promise function", async function () {
+      const res1 = map(async (a) => a, [1, 2, 3, 4]);
+
+      expect(await Promise.all(res1)).toEqual([1, 2, 3, 4]);
+
+      const res2 = pipe(
+        [1, 2, 3, 4],
+        map(async (a) => a),
+      );
+
+      expect(await Promise.all(res2)).toEqual([1, 2, 3, 4]);
+    });
 
     it("should be able to be used as a chaining method in the `fx`", async function () {
       const res = await fx(toAsync([1, 2, 3, 4]))
diff --git a/type-check/Lazy/map.test.ts b/type-check/Lazy/map.test.ts
index 093b13ec..70288969 100644
--- a/type-check/Lazy/map.test.ts
+++ b/type-check/Lazy/map.test.ts
@@ -30,6 +30,12 @@ const res10 = pipe(
   map(async (a) => a),
 );
 
+const res11 = map(async (a) => a, [1, 2, 3, 4]);
+const res12 = pipe(
+  [1, 2, 3, 4],
+  map(async (a) => a),
+);
+
 checks([
   check<typeof res1, IterableIterator<number>, Test.Pass>(),
   check<typeof res2, IterableIterator<string>, Test.Pass>(),
@@ -42,4 +48,7 @@ checks([
   check<typeof res8, IterableIterator<Promise<number>>, Test.Pass>(),
   check<typeof res9, AsyncIterableIterator<number>, Test.Pass>(),
   check<typeof res10, AsyncIterableIterator<number>, Test.Pass>(),
+
+  check<typeof res11, IterableIterator<Promise<number>>, Test.Pass>(),
+  check<typeof res12, IterableIterator<Promise<number>>, Test.Pass>(),
 ]);
diff --git a/type-check/throwError.test.ts b/type-check/throwError.test.ts
index ddd5dfda..e29499b6 100644
--- a/type-check/throwError.test.ts
+++ b/type-check/throwError.test.ts
@@ -11,7 +11,7 @@ const res2 = pipe(
 );
 
 const res3 = pipe(
-  0,
+  0 as const,
 
   unless(
     isNumber,
@@ -31,6 +31,6 @@ const res4 = pipe(
 checks([
   check<typeof res1, never, Test.Pass>(),
   check<typeof res2, never, Test.Pass>(),
-  check<typeof res3, number, Test.Pass>(),
-  check<typeof res4, number, Test.Pass>(),
+  check<typeof res3, 0, Test.Pass>(),
+  check<typeof res4, never, Test.Pass>(),
 ]);