Repository: marpple/FxTS
Pull request #162 (2022-05-25 09:19:19)

feat: implement `compactObject`
Add `compactObject` function.


Patch:
diff --git a/src/compactObject.ts b/src/compactObject.ts
new file mode 100644
index 00000000..c3df1874
--- /dev/null
+++ b/src/compactObject.ts
@@ -0,0 +1,67 @@
+import isNil from "./isNil";
+import Merge from "./types/Merge";
+
+type MapNonNullableEntries<T extends object> = {
+  [K in keyof T]: [K, Exclude<T[K], null | undefined>];
+};
+
+type MapRequiredKeys<T extends object> = {
+  [K in keyof T]: null extends Extract<T[K], null>
+    ? never
+    : undefined extends Extract<T[K], undefined>
+    ? never
+    : K;
+};
+
+type MapOptionalKeys<T extends object> = {
+  [K in keyof T]: Exclude<T[K], null | undefined> extends never
+    ? never
+    : T[K] extends Exclude<T[K], null | undefined>
+    ? never
+    : K;
+};
+
+type RequiredKeys<T extends object, U = MapRequiredKeys<T>> = Exclude<
+  U[keyof U],
+  never
+>;
+
+type OptionalKeys<T extends object, U = MapOptionalKeys<T>> = Exclude<
+  U[keyof U],
+  never
+>;
+
+type NonNullableEntries<
+  T extends object,
+  U = MapNonNullableEntries<T>,
+> = Exclude<U[keyof U], [any, never]>;
+
+/**
+ * Returns an object with all nullable values removed.
+ *
+ * @example
+ * ```ts
+ * const compacted = compactObject({ a: 1, b: "b", c: null, d: undefined });
+ * // {a: 1, b: "b"}
+ * ```
+ */
+export default function compactObject<T extends object>(
+  obj: T,
+): Merge<
+  {
+    [K in Extract<keyof T, RequiredKeys<T>>]: Extract<
+      NonNullableEntries<T>,
+      [K, any]
+    >[1];
+  },
+  {
+    [K in Extract<keyof T, OptionalKeys<T>>]?: Extract<
+      NonNullableEntries<T>,
+      [K, any]
+    >[1];
+  }
+> {
+  return Object.fromEntries(
+    Object.entries(obj).filter(([, value]) => !isNil(value)),
+  ) as any;
+}
diff --git a/src/index.ts b/src/index.ts
index 718c1c61..7c0ba6d4 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -1,6 +1,7 @@
 import add from "./add";
 import apply from "./apply";
 import average from "./average";
+import compactObject from "./compactObject";
 import consume from "./consume";
 import countBy from "./countBy";
 import curry from "./curry";
@@ -57,6 +58,7 @@ export {
   apply,
   average,
   average as mean,
+  compactObject,
   consume,
   countBy,
   curry,
diff --git a/src/types/Merge.ts b/src/types/Merge.ts
new file mode 100644
index 00000000..0767694e
--- /dev/null
+++ b/src/types/Merge.ts
@@ -0,0 +1,5 @@
+type Merge<A extends object, B extends object> = {
+  [K in keyof (A & B)]: (A & B)[K];
+} & unknown;
+
+export default Merge;
diff --git a/website/function.json b/website/function.json
index 0e9b7321..21f9b972 100644
--- a/website/function.json
+++ b/website/function.json
@@ -47,6 +47,7 @@
     "add",
     "apply",
     "average",
+    "compactObject",
     "consume",
     "countBy",
     "curry",


Test patch:
diff --git a/test/compactObject.spec.ts b/test/compactObject.spec.ts
new file mode 100644
index 00000000..c58a93d1
--- /dev/null
+++ b/test/compactObject.spec.ts
@@ -0,0 +1,18 @@
+import { compactObject } from "../src";
+
+describe("compactObject", function () {
+  it("should return identity object if there is no null or undefined property", function () {
+    const obj = { a: 1, b: "b" };
+    expect(compactObject(obj)).toEqual(obj);
+  });
+
+  it("should return an object with properties that are null or undefined removed", function () {
+    const obj = { a: 1, b: "b", c: undefined, d: null };
+    expect(compactObject(obj)).toEqual({ a: 1, b: "b" });
+  });
+
+  it("should not removed any falsy values other than null or undefined", function () {
+    const obj = { a: 0, b: "", c: undefined, d: null, e: false };
+    expect(compactObject(obj)).toEqual({ a: 0, b: "", e: false });
+  });
+});
diff --git a/type-check/compactObject.test.ts b/type-check/compactObject.test.ts
new file mode 100644
index 00000000..81fd50a4
--- /dev/null
+++ b/type-check/compactObject.test.ts
@@ -0,0 +1,29 @@
+import * as Test from "../src/types/Test";
+import compactObject from "../src/compactObject";
+
+const { checks, check } = Test;
+
+type Input = {
+  a: number;
+  b?: string;
+  c: boolean | null;
+  d: number | undefined;
+  e: string | null | undefined;
+  f?: string | null | undefined;
+  g: undefined;
+  h: null;
+  i: null | undefined;
+};
+
+type Result = {
+  a: number;
+  b?: string;
+  c?: boolean;
+  d?: number;
+  e?: string;
+  f?: string;
+};
+
+const res = compactObject({} as Input);
+
+checks([check<typeof res, Result, Test.Pass>()]);