Repository: remeda/remeda
Pull request #734 (2024-06-18 13:54:36)

feat(split): Add split
Using the type-fest split type, but extending it to support arbitrary strings, and to include the limit param.

---

Make sure that you:

- [x] Typedoc added for new methods and updated for changed
- [x] Tests added for new methods and updated for changed
- [x] New methods added to `src/index.ts`
- [x] New methods added to `docs/src/content/mapping` for both Lodash and Ramda.

---


codesandbox-ci[bot]: This pull request is automatically built and testable in [CodeSandbox](https://codesandbox.io).

  To see build info of the built libraries, click [here](https://ci.codesandbox.io/status/remeda/remeda/pr/734/builds/515358) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit 87e54fcbf3de514d174cd13161a1cafaad021f8d:

|Sandbox| Source |
|--|--|
|[remeda-example-vanilla](https://codesandbox.io/s/remeda-example-vanilla-gfjhf4)| Configuration |

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remeda/remeda/pull/734?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 100.00%. Comparing base [(`e3a8214`)](https://app.codecov.io/gh/remeda/remeda/commit/e3a821408cf7f92cae5e09ce392cd6b7eb483cb8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) to head [(`87e54fc`)](https://app.codecov.io/gh/remeda/remeda/commit/87e54fcbf3de514d174cd13161a1cafaad021f8d?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).


<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              main      #734    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          147       148     +1     
  Lines        10185     10299   +114     
  Branches       731       734     +3     
==========================================
+ Hits         10185     10299   +114     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/remeda/remeda/pull/734?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).


Patch:
diff --git a/README.md b/README.md
index 1cdeee0..19a8bb2 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,24 @@
 
 The first "data-first" and "data-last" utility library designed especially for TypeScript.
 
-![GitHub CI](https://img.shields.io/github/actions/workflow/status/remeda/remeda/ci.yml?branch=main&label=github-ci)
-[![Codecov](https://img.shields.io/codecov/c/github/remeda/remeda/main)](https://codecov.io/gh/remeda/remeda)
-[![NPM](https://img.shields.io/npm/v/remeda)](https://www.npmjs.org/package/remeda)
-![Dependencies](https://img.shields.io/librariesio/release/npm/remeda)
+[![GitHub License](https://img.shields.io/github/license/remeda/remeda?style=flat-square)](https://github.com/remeda/remeda?tab=MIT-1-ov-file#readme)
+![GitHub top language](https://img.shields.io/github/languages/top/remeda/remeda?logo=typescript&style=flat-square)
+[![NPM](https://img.shields.io/npm/v/remeda?logo=npm&style=flat-square)](https://www.npmjs.org/package/remeda)
+[![NPM Downloads](https://img.shields.io/npm/dm/remeda?logo=npm&style=flat-square)](https://npmtrends.com/remeda)
+![GitHub Repo stars](https://img.shields.io/github/stars/remeda/remeda?logo=github&style=flat-square)
+[![Libraries.io SourceRank](https://img.shields.io/librariesio/sourcerank/npm/remeda?logo=librariesdotio&style=flat-square)](https://libraries.io/npm/remeda/sourcerank)<br />
+[![Codecov](https://img.shields.io/codecov/c/github/remeda/remeda?logo=codecov&style=flat-square)](https://codecov.io/gh/remeda/remeda)
+[![GitHub branch status](https://img.shields.io/github/checks-status/remeda/remeda/main?logo=github&style=flat-square)](https://github.com/remeda/remeda/actions/workflows)
+[![GitHub Release Date](https://img.shields.io/github/release-date/remeda/remeda?logo=npm&style=flat-square)](https://github.com/remeda/remeda/releases)
+[![Commits](https://img.shields.io/github/commit-activity/y/remeda/remeda?logo=github&style=flat-square)](https://github.com/remeda/remeda/commits/main/)
+[![All contributors](https://img.shields.io/github/contributors/remeda/remeda?logo=github&style=flat-square)](https://github.com/remeda/remeda/graphs/contributors)
+
 
 ## Documentation
 
 Read the full docs and API reference on [remedajs.com/docs](https://remedajs.com/docs).
 
-Migrating from Lodash or Ramda? Check the function mapping on [remedajs.com/mapping](https://remedajs.com/mapping).
+Migrating from other libraries? Check out our migration guides for [Lodash](https://remedajs.com/migrate/lodash) and [Ramda](https://remedajs.com/migrate/ramda)!
 
 ## Features
 
diff --git a/docs/src/content/mapping/lodash/__MISSING.md b/docs/src/content/mapping/lodash/__MISSING.md
index ea20efc..aeb2dc7 100644
--- a/docs/src/content/mapping/lodash/__MISSING.md
+++ b/docs/src/content/mapping/lodash/__MISSING.md
@@ -144,7 +144,6 @@ TODO: Go over: https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#
 - repeat
 - replace
 - snakeCase
-- split
 - startCase
 - startsWith
 - template
diff --git a/docs/src/content/mapping/lodash/split.md b/docs/src/content/mapping/lodash/split.md
new file mode 100644
index 0000000..d37386c
--- /dev/null
+++ b/docs/src/content/mapping/lodash/split.md
@@ -0,0 +1,4 @@
+---
+category: String
+remeda: split
+---
diff --git a/docs/src/content/mapping/ramda/__MISSING.md b/docs/src/content/mapping/ramda/__MISSING.md
index 54c024b..1d3a898 100644
--- a/docs/src/content/mapping/ramda/__MISSING.md
+++ b/docs/src/content/mapping/ramda/__MISSING.md
@@ -166,6 +166,16 @@
 - union
 - unionWith
 
+# String
+
+- match
+- replace
+- test
+- toLower
+- toString
+- toUpper
+- trim
+
 # Type
 
 - type
diff --git a/docs/src/content/mapping/ramda/split.md b/docs/src/content/mapping/ramda/split.md
new file mode 100644
index 0000000..d37386c
--- /dev/null
+++ b/docs/src/content/mapping/ramda/split.md
@@ -0,0 +1,4 @@
+---
+category: String
+remeda: split
+---
diff --git a/src/fromKeys.ts b/src/fromKeys.ts
index bf6cdac..9c196a5 100644
--- a/src/fromKeys.ts
+++ b/src/fromKeys.ts
@@ -1,5 +1,5 @@
 import type { Simplify } from "type-fest";
-import type { IterableContainer } from "./internal/types";
+import type { ExactRecord, IterableContainer } from "./internal/types";
 import { purry } from "./purry";
 
 // Takes a union of literals and creates a union of records with the value V for
@@ -13,7 +13,7 @@ type FromKeys<T extends IterableContainer, V> = T extends readonly []
   : T extends readonly [infer Head, ...infer Rest]
     ? ExactlyOneKey<Head, V> & FromKeys<Rest, V>
     : T[number] extends PropertyKey
-      ? Partial<Record<T[number], V>>
+      ? ExactRecord<T[number], V>
       : never;
 
 /**
diff --git a/src/index.ts b/src/index.ts
index 330fc21..daf2993 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -113,6 +113,7 @@ export * from "./sortedIndexWith";
 export * from "./sortedLastIndex";
 export * from "./sortedLastIndexBy";
 export * from "./splice";
+export * from "./split";
 export * from "./splitAt";
 export * from "./splitWhen";
 export * from "./stringToPath";
diff --git a/src/pullObject.ts b/src/pullObject.ts
index e105e9f..0832d77 100644
--- a/src/pullObject.ts
+++ b/src/pullObject.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { ExactRecord, IterableContainer } from "./internal/types";
 import { purry } from "./purry";
 
 /**
@@ -40,7 +40,7 @@ export function pullObject<
   data: T,
   keyExtractor: (item: T[number], index: number, data: T) => K,
   valueExtractor: (item: T[number], index: number, data: T) => V,
-): Partial<Record<K, V>>;
+): ExactRecord<K, V>;
 
 /**
  * Creates an object that maps the result of `valueExtractor` with a key
@@ -78,7 +78,7 @@ export function pullObject<
 >(
   keyExtractor: (item: T[number], index: number, data: T) => K,
   valueExtractor: (item: T[number], index: number, data: T) => V,
-): (data: T) => Partial<Record<K, V>>;
+): (data: T) => ExactRecord<K, V>;
 
 export function pullObject(...args: ReadonlyArray<unknown>): unknown {
   return purry(pullObjectImplementation, args);
diff --git a/src/split.ts b/src/split.ts
new file mode 100644
index 0000000..49db482
--- /dev/null
+++ b/src/split.ts
@@ -0,0 +1,114 @@
+import type {
+  ArraySlice,
+  IsFloat,
+  NonNegative,
+  Split as SplitBase,
+} from "type-fest";
+
+type Split<
+  S extends string,
+  Separator extends string,
+  N extends number | undefined = undefined,
+> = string extends S
+  ? Array<string>
+  : string extends Separator
+    ? Array<string>
+    : number extends N
+      ? Array<string>
+      : // TODO: We need a way to "floor" non-integer numbers, until then we return a lower fidelity type instead.
+        IsFloat<N> extends true
+        ? Array<string>
+        : ArraySlice<
+            // We can use the base (type-fest) split **only** if all the params
+            // are literals. For all other cases it would return the wrong type
+            // so we fallback to the built-in Array.prototype.split return type.
+            SplitBase<S, Separator>,
+            0,
+            // `undefined` and negative numbers are treated as non-limited
+            // splits, which is what we'd get when using `never` for ArraySlice.
+            N extends number ? NonNegative<N> : never
+          >;
+
+/**
+ * Takes a pattern and divides this string into an ordered list of substrings by
+ * searching for the pattern, puts these substrings into an array, and returns
+ * the array. This function mirrors the built-in [`String.prototype.split`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split)
+ * method.
+ *
+ * @param data - The string to split.
+ * @param separator - The pattern describing where each split should occur. Can
+ * be a string, or a regular expression.
+ * @param limit - A non-negative integer specifying a limit on the number of
+ * substrings to be included in the array. If provided, splits the string at
+ * each occurrence of the specified separator, but stops when limit entries have
+ * been placed in the array. Any leftover text is not included in the array at
+ * all. The array may contain fewer entries than limit if the end of the string
+ * is reached before the limit is reached. If limit is 0, [] is returned.
+ * @returns An Array of strings, split at each point where the separator occurs
+ * in the given string.
+ * @signature
+ *   R.split(data, separator, limit);
+ * @example
+ *   R.split("a,b,c", ","); //=> ["a", "b", "c"]
+ *   R.split("a,b,c", ",", 2); //=> ["a", "b"]
+ *   R.split("a1b2c3d", /\d/u); //=> ["a", "b", "c", "d"]
+ * @dataFirst
+ * @category String
+ */
+export function split(
+  data: string,
+  separator: RegExp,
+  limit?: number,
+): Array<string>;
+export function split<
+  S extends string,
+  Separator extends string,
+  N extends number | undefined = undefined,
+>(data: S, separator: Separator, limit?: N): Split<S, Separator, N>;
+
+/**
+ * Takes a pattern and divides this string into an ordered list of substrings by
+ * searching for the pattern, puts these substrings into an array, and returns
+ * the array. This function mirrors the built-in [`String.prototype.split`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split)
+ * method.
+ *
+ * @param separator - The pattern describing where each split should occur. Can
+ * be a string, or a regular expression.
+ * @param limit - A non-negative integer specifying a limit on the number of
+ * substrings to be included in the array. If provided, splits the string at
+ * each occurrence of the specified separator, but stops when limit entries have
+ * been placed in the array. Any leftover text is not included in the array at
+ * all. The array may contain fewer entries than limit if the end of the string
+ * is reached before the limit is reached. If limit is 0, [] is returned.
+ * @returns An Array of strings, split at each point where the separator occurs
+ * in the given string.
+ * @signature
+ *   R.split(separator, limit)(data);
+ * @example
+ *   R.pipe("a,b,c", R.split(",")); //=> ["a", "b", "c"]
+ *   R.pipe("a,b,c", R.split(",", 2)); //=> ["a", "b"]
+ *   R.pipe("a1b2c3d", R.split(/\d/u)); //=> ["a", "b", "c", "d"]
+ * @dataLast
+ * @category String
+ */
+export function split(
+  separator: RegExp,
+  limit?: number,
+): (data: string) => Array<string>;
+export function split<
+  S extends string,
+  Separator extends string,
+  N extends number | undefined = undefined,
+>(separator: Separator, limit?: N): (data: S) => Split<S, Separator, N>;
+
+export function split(
+  dataOrSeparator: RegExp | string,
+  separatorOrLimit?: RegExp | number | string,
+  limit?: number,
+): unknown {
+  return typeof separatorOrLimit === "number" || separatorOrLimit === undefined
+    ? // dataLast
+      (data: string) => data.split(dataOrSeparator, separatorOrLimit)
+    : // dataFirst
+      (dataOrSeparator as string).split(separatorOrLimit, limit);
+}


Test patch:
diff --git a/src/fromKeys.test.ts b/src/fromKeys.test.ts
index 140b608..9c54dc0 100644
--- a/src/fromKeys.test.ts
+++ b/src/fromKeys.test.ts
@@ -1,5 +1,6 @@
 import type { Simplify } from "type-fest";
 import { add } from "./add";
+import { constant } from "./constant";
 import { fromKeys } from "./fromKeys";
 import { pipe } from "./pipe";
 
@@ -71,74 +72,74 @@ describe("runtime", () => {
       [symbol]: "symbol",
     });
   });
-});
 
-describe("dataLast", () => {
-  it("works on trivially empty arrays", () => {
-    expect(
-      pipe(
-        [] as Array<string>,
-        fromKeys((item) => `${item}_`),
-      ),
-    ).toEqual({});
-  });
+  describe("dataLast", () => {
+    it("works on trivially empty arrays", () => {
+      expect(
+        pipe(
+          [] as Array<string>,
+          fromKeys((item) => `${item}_`),
+        ),
+      ).toEqual({});
+    });
 
-  it("works on regular arrays", () => {
-    expect(
-      pipe(
-        ["a"],
-        fromKeys((item) => `${item}_`),
-      ),
-    ).toEqual({ a: "a_" });
-  });
+    it("works on regular arrays", () => {
+      expect(
+        pipe(
+          ["a"],
+          fromKeys((item) => `${item}_`),
+        ),
+      ).toEqual({ a: "a_" });
+    });
 
-  it("works with duplicates", () => {
-    expect(
-      pipe(
-        ["a", "a"],
-        fromKeys((item) => `${item}_`),
-      ),
-    ).toEqual({ a: "a_" });
-  });
+    it("works with duplicates", () => {
+      expect(
+        pipe(
+          ["a", "a"],
+          fromKeys((item) => `${item}_`),
+        ),
+      ).toEqual({ a: "a_" });
+    });
 
-  it("uses the last value", () => {
-    let counter = 0;
-    expect(
-      pipe(
-        ["a", "a"],
-        fromKeys(() => {
-          counter += 1;
-          return counter;
-        }),
-      ),
-    ).toEqual({ a: 2 });
-  });
+    it("uses the last value", () => {
+      let counter = 0;
+      expect(
+        pipe(
+          ["a", "a"],
+          fromKeys(() => {
+            counter += 1;
+            return counter;
+          }),
+        ),
+      ).toEqual({ a: 2 });
+    });
 
-  it("works with number keys", () => {
-    expect(pipe([123], fromKeys(add(1)))).toEqual({ 123: 124 });
-  });
+    it("works with number keys", () => {
+      expect(pipe([123], fromKeys(add(1)))).toEqual({ 123: 124 });
+    });
 
-  it("works with symbols", () => {
-    const symbol = Symbol("a");
-    expect(
-      pipe(
-        [symbol],
-        fromKeys(() => 1),
-      ),
-    ).toEqual({ [symbol]: 1 });
-  });
+    it("works with symbols", () => {
+      const symbol = Symbol("a");
+      expect(
+        pipe(
+          [symbol],
+          fromKeys(() => 1),
+        ),
+      ).toEqual({ [symbol]: 1 });
+    });
 
-  it("works with a mix of key types", () => {
-    const symbol = Symbol("a");
-    expect(
-      pipe(
-        ["a", 123, symbol],
-        fromKeys((item) => typeof item),
-      ),
-    ).toEqual({
-      a: "string",
-      123: "number",
-      [symbol]: "symbol",
+    it("works with a mix of key types", () => {
+      const symbol = Symbol("a");
+      expect(
+        pipe(
+          ["a", 123, symbol],
+          fromKeys((item) => typeof item),
+        ),
+      ).toEqual({
+        a: "string",
+        123: "number",
+        [symbol]: "symbol",
+      });
     });
   });
 });
@@ -147,11 +148,11 @@ describe("typing", () => {
   test("empty array", () => {
     const data = [] as const;
 
-    const dataFirst = fromKeys(data, getVal);
+    const dataFirst = fromKeys(data, constant(1));
     // eslint-disable-next-line @typescript-eslint/ban-types -- That's just what we return
     expectTypeOf(dataFirst).toEqualTypeOf<{}>();
 
-    const dataLast = pipe(data, fromKeys(getVal));
+    const dataLast = pipe(data, fromKeys(constant(1)));
     // eslint-disable-next-line @typescript-eslint/ban-types -- That's just what we return
     expectTypeOf(dataLast).toEqualTypeOf<{}>();
   });
@@ -159,10 +160,10 @@ describe("typing", () => {
   test("fixed tuple", () => {
     const data = ["cat", "dog"] as const;
 
-    const dataFirst = fromKeys(data, getVal);
+    const dataFirst = fromKeys(data, constant(1));
     expectTypeOf(dataFirst).toEqualTypeOf<Record<"cat" | "dog", number>>();
 
-    const dataLast = pipe(data, fromKeys(getVal));
+    const dataLast = pipe(data, fromKeys(constant(1)));
     expectTypeOf(dataLast).toEqualTypeOf<Record<"cat" | "dog", number>>();
   });
 
@@ -170,30 +171,30 @@ describe("typing", () => {
     test("regular array", () => {
       const data = [] as Array<string>;
 
-      const dataFirst = fromKeys(data, getVal);
-      expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<string, number>>>();
+      const dataFirst = fromKeys(data, constant(1));
+      expectTypeOf(dataFirst).toEqualTypeOf<Record<string, number>>();
 
-      const dataLast = pipe(data, fromKeys(getVal));
-      expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<string, number>>>();
+      const dataLast = pipe(data, fromKeys(constant(1)));
+      expectTypeOf(dataLast).toEqualTypeOf<Record<string, number>>();
     });
 
     test("non-empty array", () => {
       const data = ["cat"] as [string, ...Array<string>];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<Record<string, number>>();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<Record<string, number>>();
     });
 
     test("fixed tuple", () => {
       const data = ["cat", "dog"] as [string, string];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<Record<string, number>>();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<Record<string, number>>();
     });
   });
@@ -202,12 +203,12 @@ describe("typing", () => {
     test("regular array", () => {
       const data = [] as Array<"cat" | "dog">;
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Partial<Record<"cat" | "dog", number>>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Partial<Record<"cat" | "dog", number>>
       >();
@@ -216,7 +217,7 @@ describe("typing", () => {
     test("non-empty array", () => {
       const data = ["cat"] as ["cat" | "dog", ...Array<"mouse" | "pig">];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Simplify<
           Partial<Record<"mouse" | "pig", number>> &
@@ -224,7 +225,7 @@ describe("typing", () => {
         >
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Simplify<
           Partial<Record<"mouse" | "pig", number>> &
@@ -236,7 +237,7 @@ describe("typing", () => {
     test("fixed tuple", () => {
       const data = ["cat", "mouse"] as ["cat" | "dog", "mouse" | "pig"];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Simplify<
           ({ cat: number } | { dog: number }) &
@@ -244,7 +245,7 @@ describe("typing", () => {
         >
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Simplify<
           ({ cat: number } | { dog: number }) &
@@ -258,14 +259,14 @@ describe("typing", () => {
     test("regular array", () => {
       const data = [] as Array<`prefix_${number}`>;
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
-        Partial<Record<`prefix_${number}`, number>>
+        Record<`prefix_${number}`, number>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
-        Partial<Record<`prefix_${number}`, number>>
+        Record<`prefix_${number}`, number>
       >();
     });
 
@@ -275,12 +276,12 @@ describe("typing", () => {
         ...Array<`prefix_${number}`>,
       ];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Record<`prefix_${number}`, number>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Record<`prefix_${number}`, number>
       >();
@@ -292,12 +293,12 @@ describe("typing", () => {
         `${number}_suffix`,
       ];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Record<`${number}_suffix` | `prefix_${number}`, number>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Record<`${number}_suffix` | `prefix_${number}`, number>
       >();
@@ -307,42 +308,40 @@ describe("typing", () => {
       test("regular array", () => {
         const data = [] as Array<number>;
 
-        const dataFirst = fromKeys(data, getVal);
-        expectTypeOf(dataFirst).toEqualTypeOf<
-          Partial<Record<number, number>>
-        >();
+        const dataFirst = fromKeys(data, constant(1));
+        expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
-        expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<number, number>>>();
+        const dataLast = pipe(data, fromKeys(constant(1)));
+        expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
       });
 
       test("non-empty array", () => {
         const data = [1] as [number, ...Array<number>];
 
-        const dataFirst = fromKeys(data, getVal);
+        const dataFirst = fromKeys(data, constant(1));
         expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
+        const dataLast = pipe(data, fromKeys(constant(1)));
         expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
       });
 
       test("fixed tuple", () => {
         const data = [1, 2] as [number, number];
 
-        const dataFirst = fromKeys(data, getVal);
+        const dataFirst = fromKeys(data, constant(1));
         expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
+        const dataLast = pipe(data, fromKeys(constant(1)));
         expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
       });
 
       test("literals", () => {
         const data = [1, 2, 3] as const;
 
-        const dataFirst = fromKeys(data, getVal);
+        const dataFirst = fromKeys(data, constant(1));
         expectTypeOf(dataFirst).toEqualTypeOf<Record<1 | 2 | 3, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
+        const dataLast = pipe(data, fromKeys(constant(1)));
         expectTypeOf(dataLast).toEqualTypeOf<Record<1 | 2 | 3, number>>();
       });
     });
@@ -351,18 +350,14 @@ describe("typing", () => {
   test("typescript doesn't choke on huge literal unions", () => {
     const data = [] as Array<`${Letter}${Letter}`>;
 
-    const dataFirst = fromKeys(data, getVal);
+    const dataFirst = fromKeys(data, constant(1));
     expectTypeOf(dataFirst).toEqualTypeOf<
       Partial<Record<`${Letter}${Letter}`, number>>
     >();
 
-    const dataLast = pipe(data, fromKeys(getVal));
+    const dataLast = pipe(data, fromKeys(constant(1)));
     expectTypeOf(dataLast).toEqualTypeOf<
       Partial<Record<`${Letter}${Letter}`, number>>
     >();
   });
 });
-
-function getVal(): number {
-  return 1;
-}
diff --git a/src/pullObject.test.ts b/src/pullObject.test.ts
index 76aac03..57f96f2 100644
--- a/src/pullObject.test.ts
+++ b/src/pullObject.test.ts
@@ -174,30 +174,30 @@ describe("typing", () => {
     const data = ["a", "b"];
 
     const dataFirst = pullObject(data, identity(), constant("value"));
-    expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<string, string>>>();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<string, string>>();
 
     const dataLast = pipe(data, pullObject(identity(), constant("value")));
-    expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<string, string>>>();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<string, string>>();
   });
 
   test("number keys", () => {
     const data = [1, 2];
 
     const dataFirst = pullObject(data, identity(), constant(3));
-    expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<number, number>>>();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
     const dataLast = pipe(data, pullObject(identity(), constant(3)));
-    expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<number, number>>>();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
   });
 
   test("symbol keys", () => {
     const data = [Symbol("a"), Symbol("b")];
 
     const dataFirst = pullObject(data, identity(), constant(Symbol("c")));
-    expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<symbol, symbol>>>();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<symbol, symbol>>();
 
     const dataLast = pipe(data, pullObject(identity(), constant(Symbol("c"))));
-    expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<symbol, symbol>>>();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<symbol, symbol>>();
   });
 
   test("number constants", () => {
@@ -249,16 +249,12 @@ describe("typing", () => {
       (item) => `prefix_${item}`,
       constant("value"),
     );
-    expectTypeOf(dataFirst).toEqualTypeOf<
-      Partial<Record<`prefix_${number}`, string>>
-    >();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<`prefix_${number}`, string>>();
 
     const dataLast = pipe(
       data,
       pullObject((item) => `prefix_${item}`, constant("value")),
     );
-    expectTypeOf(dataLast).toEqualTypeOf<
-      Partial<Record<`prefix_${number}`, string>>
-    >();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<`prefix_${number}`, string>>();
   });
 });
diff --git a/src/split.test.ts b/src/split.test.ts
new file mode 100644
index 0000000..6508415
--- /dev/null
+++ b/src/split.test.ts
@@ -0,0 +1,201 @@
+import { split } from "./split";
+import { pipe } from "./pipe";
+
+describe("runtime", () => {
+  test("empty string, empty separator", () => {
+    expect(split("", "")).toEqual([]);
+  });
+
+  test("empty string, non-empty separator", () => {
+    expect(split("", ",")).toEqual([""]);
+  });
+
+  test("trivial split", () => {
+    expect(split("a", ",")).toEqual(["a"]);
+  });
+
+  test("string contains separator", () => {
+    expect(split(",", ",")).toEqual(["", ""]);
+  });
+
+  test("useful split", () => {
+    expect(split("a,b,c", ",")).toEqual(["a", "b", "c"]);
+  });
+
+  test("regex split", () => {
+    expect(split("a,b,c", /,/u)).toEqual(["a", "b", "c"]);
+  });
+
+  test("multiple types of separators", () => {
+    expect(split("a,b;c", /[;,]/u)).toEqual(["a", "b", "c"]);
+  });
+
+  test("regex with limit", () => {
+    expect(split("a,b,c", /,/u, 2)).toEqual(["a", "b"]);
+  });
+
+  test("limited split", () => {
+    expect(split("a,b,c", ",", 2)).toEqual(["a", "b"]);
+  });
+
+  test("limit is higher than splits", () => {
+    expect(split("a,b,c", ",", 5)).toEqual(["a", "b", "c"]);
+  });
+
+  test("multiple consecutive separators", () => {
+    expect(split("a,,b", ",")).toEqual(["a", "", "b"]);
+  });
+
+  test("separator at the start and end", () => {
+    expect(split(",a,b,", ",")).toEqual(["", "a", "b", ""]);
+  });
+
+  test("empty-string separator", () => {
+    expect(split("abcdef", "")).toEqual(["a", "b", "c", "d", "e", "f"]);
+  });
+
+  test("undefined limit", () => {
+    expect(split("a,b,c", ",", undefined)).toEqual(["a", "b", "c"]);
+  });
+
+  test("negative limit", () => {
+    expect(split("a,b,c", ",", -1)).toEqual(["a", "b", "c"]);
+  });
+
+  test("fractional limits", () => {
+    expect(split("a,b,c", ",", 1.5)).toEqual(["a"]);
+  });
+
+  test("0 limit", () => {
+    expect(split("a,b,c", ",", 0)).toEqual([]);
+  });
+
+  describe("dataLast", () => {
+    test("useful split", () => {
+      expect(pipe("a,b,c", split(","))).toEqual(["a", "b", "c"]);
+    });
+
+    test("regex split", () => {
+      expect(pipe("a,b,c", split(/,/u))).toEqual(["a", "b", "c"]);
+    });
+
+    test("limited split", () => {
+      expect(pipe("a,b,c", split(",", 2))).toEqual(["a", "b"]);
+    });
+
+    test("regex with limit", () => {
+      expect(pipe("a,b,c", split(/,/u, 2))).toEqual(["a", "b"]);
+    });
+
+    test("undefined limit", () => {
+      expect(pipe("a,b,c", split(",", undefined))).toEqual(["a", "b", "c"]);
+    });
+  });
+});
+
+describe("typing", () => {
+  test("non-literals", () => {
+    const result = split("" as string, "" as string);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("non-literal data", () => {
+    const result = split("" as string, ",");
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("non-literal separator", () => {
+    const result = split("", "," as string);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("non-literal limit", () => {
+    const result = split("", "", 1 as number);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("literal empty strings", () => {
+    const result = split("", "");
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+
+  test("trivial literals", () => {
+    const result = split("a", ",");
+    expectTypeOf(result).toEqualTypeOf<["a"]>();
+  });
+
+  test("trivial literals", () => {
+    const result = split("a", ",");
+    expectTypeOf(result).toEqualTypeOf<["a"]>();
+  });
+
+  test("string contains separator", () => {
+    const result = split(",", ",");
+    expectTypeOf(result).toEqualTypeOf<["", ""]>();
+  });
+
+  test("useful split", () => {
+    const result = split("a,b,c", ",");
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("regex split", () => {
+    const result = split("a,b,c", /,/u);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("limited split", () => {
+    const result = split("a,b,c", ",", 2);
+    expectTypeOf(result).toEqualTypeOf<["a", "b"]>();
+  });
+
+  test("limit is higher than splits", () => {
+    const result = split("a,b,c", ",", 5);
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("undefined limit", () => {
+    const result = split("a,b,c", ",", undefined);
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("multiple consecutive separators", () => {
+    const result = split("a,,b", ",");
+    expectTypeOf(result).toEqualTypeOf<["a", "", "b"]>();
+  });
+
+  test("separator at the start and end", () => {
+    const result = split(",a,b,", ",");
+    expectTypeOf(result).toEqualTypeOf<["", "a", "b", ""]>();
+  });
+
+  test("empty string separator", () => {
+    const result = split("abcdef", "");
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c", "d", "e", "f"]>();
+  });
+
+  test("empty everything", () => {
+    const result = split("", "");
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+
+  test("literal string with multiple character separator", () => {
+    const result = split("a--b--c", "--");
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("negative limit", () => {
+    const result = split("a,b,c", ",", -1);
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("fractional limits", () => {
+    const result = split("a,b,c", ",", 1.5);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("0 limit", () => {
+    const result = split("a,b,c", ",", 0);
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+});