Repository: marpple/FxTS (ts)

Issue:
Add `isObject` function
# Suggestion

<!--
  Please fill in each section completely. Thank you!
-->

## ⭐ Suggestion

https://lodash.com/docs/4.17.15#isObject

<!-- What would you like to be added? -->

Since the purpose of this function is to check only objects, not primitive values, it seems to be implemented in the same way as isObject of lodash.

## 💻 Use Cases

https://lodash.com/docs/4.17.15#isObject

<!-- What do you want to use this for? -->


Pull request description:
feat: add 'isObject' function
Fixes #51


Patch:
diff --git a/src/index.ts b/src/index.ts
index 6224ac75..afccfc07 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -12,6 +12,7 @@ import includes from "./includes";
 import indexBy from "./indexBy";
 import isArray from "./isArray";
 import isNil from "./isNil";
+import isObject from "./isObject";
 import isString from "./isString";
 import join from "./join";
 import last from "./last";
@@ -46,6 +47,7 @@ export {
   indexBy,
   isArray,
   isNil,
+  isObject,
   isString,
   join,
   last,
diff --git a/src/isObject.ts b/src/isObject.ts
new file mode 100644
index 00000000..3c63e861
--- /dev/null
+++ b/src/isObject.ts
@@ -0,0 +1,21 @@
+/**
+ * Checks if value is the type of object.
+ *
+ * @example
+ * ```ts
+ * isObject({}); // true
+ * isObject([1, 2, 3]); // true
+ * isObject(() => {}); // true
+ * isObject(null); // false
+ * isObject(123); // false
+ * ```
+ */
+
+type IdentityObject<T> = T extends object ? T : never;
+
+function isObject<T = unknown>(a: T): a is IdentityObject<T> {
+  const type = typeof a;
+  return a != null && (type === "object" || type === "function");
+}
+
+export default isObject;


Test patch:
diff --git a/test/Lazy/concurrent.spec.ts b/test/Lazy/concurrent.spec.ts
index 7660a715..8e03eac0 100644
--- a/test/Lazy/concurrent.spec.ts
+++ b/test/Lazy/concurrent.spec.ts
@@ -1,4 +1,13 @@
-import { concurrent, delay, map, pipe, range, toAsync } from "../../src/index";
+import {
+  concurrent,
+  delay,
+  filter,
+  map,
+  peek,
+  pipe,
+  range,
+  toAsync,
+} from "../../src/index";
 import { callFuncAfterTime } from "../utils";
 
 describe("concurrent", function () {
@@ -51,6 +60,47 @@ describe("concurrent", function () {
     expect(arr).toEqual([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]);
   }, 550);
 
+  it("should be affected only one concurrent below it, when nested concurrent", async function () {
+    let fn: jest.Mock<any, any>;
+    fn = jest.fn();
+    let concurrent10Count = 0;
+    let concurrent2Count = 0;
+
+    const iter = pipe(
+      toAsync(range(Infinity)),
+      peek(() => concurrent10Count++),
+      map((a) => delay(100, a)),
+      concurrent(10),
+      peek(() => concurrent2Count++),
+      map((a) => delay(100, a)),
+      filter((a) => a % 2 === 0),
+      concurrent(2),
+    );
+
+    callFuncAfterTime(fn, 300);
+    await iter.next();
+    await iter.next();
+    expect(fn).toBeCalled();
+    expect(concurrent2Count).toEqual(4);
+    expect(concurrent10Count).toEqual(10);
+
+    fn = jest.fn();
+    callFuncAfterTime(fn, 200);
+    await iter.next();
+    await iter.next();
+    expect(fn).toBeCalled();
+    expect(concurrent2Count).toEqual(8);
+    expect(concurrent10Count).toEqual(10);
+
+    fn = jest.fn();
+    callFuncAfterTime(fn, 300);
+    await iter.next();
+    await iter.next();
+    expect(fn).toBeCalled();
+    expect(concurrent2Count).toEqual(12);
+    expect(concurrent10Count).toEqual(20);
+  }, 850);
+
   it("should return IteratorReturnResult after all consuming 'AsyncIterable'", async function () {
     const fn = jest.fn();
     callFuncAfterTime(fn, 1000);
diff --git a/test/isObject.spec.ts b/test/isObject.spec.ts
new file mode 100644
index 00000000..bf66c103
--- /dev/null
+++ b/test/isObject.spec.ts
@@ -0,0 +1,22 @@
+import { isObject } from "../src";
+
+describe("isObject", function () {
+  it.each([
+    [[], true],
+    [{}, true],
+    [() => "", true],
+    [Promise.resolve(), true],
+    [class A {}, true],
+    [new (class A {})(), true],
+    [123, false],
+    ["abc", false],
+    [Symbol("symbol"), false],
+    [null, false],
+    [undefined, false],
+  ])(
+    "should return a boolean value that given value is object or not",
+    function (value, expected) {
+      expect(isObject(value)).toEqual(expected);
+    },
+  );
+});
diff --git a/type-check/isObject.test.ts b/type-check/isObject.test.ts
new file mode 100644
index 00000000..b60fd61b
--- /dev/null
+++ b/type-check/isObject.test.ts
@@ -0,0 +1,31 @@
+import * as Test from "../src/types/Test";
+import { isObject } from "../src";
+
+const { checks, check } = Test;
+
+const res1 = isObject({});
+const res2 = isObject("a");
+const res3 = isObject(() => "");
+const res4 = isObject(Math.random() > 0.5 ? {} : null);
+
+type Node = {
+  id: number;
+  name: string;
+  child?: Node | number;
+};
+
+function getChildNodeId(node: Node) {
+  if (isObject(node.child)) {
+    return node.child.id;
+  } else {
+    return node.child ?? -1;
+  }
+}
+
+checks([
+  check<typeof res1, boolean, Test.Pass>(),
+  check<typeof res2, boolean, Test.Pass>(),
+  check<typeof res3, boolean, Test.Pass>(),
+  check<typeof res4, boolean, Test.Pass>(),
+  check<ReturnType<typeof getChildNodeId>, number, Test.Pass>(),
+]);