Repository: remeda/remeda (ts)

Issue:
Add partial, partialRight
We're hoping to replace lodash and ramda entirely with remeda. There are a few functions remeda doesn't have that I believe it could benefit from.

Lodash and ramda both have `partial`, and `partialRight` methods and I don't see any replacements in remeda yet. We use lodash's `parseInt` which is sort of a partialRight of `Number.parseInt` with the radix of 10 already applied. I was going to write my own but noticed there's no partial/currying methods yet.

[Lodash docs](https://lodash.com/docs/4.17.15#partial)
[Lodash source](https://github.com/lodash/lodash/blob/main/src/partial.ts)

Pull request description:
feat(partialBind): Add partialBind, partialRightBind
part of #458 

typing data-last was not working well, and i decided it was not super useful in a data-last context anyway, so i omitted it

a partialBindObj function is in #855 

Patch:
diff --git a/docs/package.json b/docs/package.json
index 4fd53e0..0328d32 100644
--- a/docs/package.json
+++ b/docs/package.json
@@ -7,15 +7,14 @@
   "type": "module",
   "scripts": {
     "build": "astro check && npm run build:scripts && astro build",
-    "build:all": "npm run clean && npm run typedoc && npm run build",
+    "build:all": "npm run clean && typedoc && npm run build",
     "build:debug": "npm run build && open-cli node_modules/.cache/bundleVisualizer.html",
     "build:scripts": "vite build --config vite.scripts.config.ts",
     "check": "tsc && astro check",
     "clean": "rm -rf dist && rm -f src/data/data.json && rm -rf public/dist",
     "dev": "astro dev",
     "lint": "eslint",
-    "preview": "astro preview",
-    "typedoc": "typedoc --json src/data/data.json"
+    "preview": "astro preview"
   },
   "dependencies": {
     "@astrojs/check": "^0.9.3",
diff --git a/docs/src/content/mapping/lodash/partial.md b/docs/src/content/mapping/lodash/partial.md
new file mode 100644
index 0000000..1b01984
--- /dev/null
+++ b/docs/src/content/mapping/lodash/partial.md
@@ -0,0 +1,24 @@
+---
+category: Function
+remeda: partialBind
+---
+
+The placeholder argument `_` is not supported. Some cases can be written as `partialLastBind`; otherwise, use an arrow function.
+
+```ts
+function greet(greeting, firstName, lastName) {
+  return greeting + " " + firstName + " " + lastName;
+}
+
+// Lodash
+_.partial(greet, _, "john", "doe");
+_.partial(greet, "hi", _, "doe");
+
+// Remeda
+partialLastBind(greet, "john", "doe");
+// (not supported)
+
+// Native
+(greeting) => greet(greeting, "john", "doe");
+(firstName) => greet("hi", firstName, "doe");
+```
diff --git a/docs/src/content/mapping/lodash/partialRight.md b/docs/src/content/mapping/lodash/partialRight.md
new file mode 100644
index 0000000..6b774c4
--- /dev/null
+++ b/docs/src/content/mapping/lodash/partialRight.md
@@ -0,0 +1,24 @@
+---
+category: Function
+remeda: partialLastBind
+---
+
+The placeholder argument `_` is not supported. Some cases can be written as `partialBind`; otherwise, use an arrow function.
+
+```ts
+function greet(greeting, firstName, lastName) {
+  return greeting + " " + firstName + " " + lastName;
+}
+
+// Lodash
+_.partialRight(greet, "hi", "john", _);
+_.partialRight(greet, "hi", _, "doe");
+
+// Remeda
+partialBind(greet, ["hi", "john"]);
+// (not supported)
+
+// Native
+(lastName) => greet("hi", "john", lastName);
+(firstName) => greet("hi", firstName, "doe");
+```
diff --git a/docs/src/content/mapping/ramda/__MISSING.md b/docs/src/content/mapping/ramda/__MISSING.md
index 6c85119..4078031 100644
--- a/docs/src/content/mapping/ramda/__MISSING.md
+++ b/docs/src/content/mapping/ramda/__MISSING.md
@@ -34,9 +34,7 @@
 - of
 - on
 - otherwise
-- partial
 - partialObject
-- partialRight
 - pipeWith
 - promap
 - thunkify
diff --git a/docs/src/content/mapping/ramda/partial.md b/docs/src/content/mapping/ramda/partial.md
new file mode 100644
index 0000000..8a3e058
--- /dev/null
+++ b/docs/src/content/mapping/ramda/partial.md
@@ -0,0 +1,18 @@
+---
+category: Function
+remeda: partialBind
+---
+
+Remeda's `partialBind` takes a variadic list of arguments instead of an array.
+
+```ts
+function greet(greeting, firstName, lastName) {
+  return greeting + " " + firstName + " " + lastName;
+}
+
+// Ramda
+partial(greet, ["hi", "john"]);
+
+// Remeda
+partialBind(greet, "hi", "john");
+```
diff --git a/docs/src/content/mapping/ramda/partialRight.md b/docs/src/content/mapping/ramda/partialRight.md
new file mode 100644
index 0000000..8115b68
--- /dev/null
+++ b/docs/src/content/mapping/ramda/partialRight.md
@@ -0,0 +1,18 @@
+---
+category: Function
+remeda: partialLastBind
+---
+
+Remeda's `partialLastBind` takes a variadic list of arguments instead of an array.
+
+```ts
+function greet(greeting, firstName, lastName) {
+  return greeting + " " + firstName + " " + lastName;
+}
+
+// Ramda
+partialRight(greet, ["john", "doe"]);
+
+// Remeda
+partialLastBind(greet, "john", "doe");
+```
diff --git a/docs/typedoc.config.js b/docs/typedoc.config.js
new file mode 100644
index 0000000..11925ef
--- /dev/null
+++ b/docs/typedoc.config.js
@@ -0,0 +1,25 @@
+import { OptionDefaults } from "typedoc";
+
+/** @type {Partial<import('typedoc').TypeDocOptions>} */
+export default {
+  json: "src/data/data.json",
+
+  tsconfig: "../tsconfig.json",
+  entryPoints: ["../src/index.ts"],
+  exclude: ["**/*.test.ts", "**/*.test-d.ts"],
+
+  jsDocCompatibility: {
+    exampleTag: false,
+  },
+
+  blockTags: [
+    ...OptionDefaults.blockTags,
+    "@dataFirst",
+    "@dataLast",
+    "@lazy",
+    "@signature",
+  ],
+
+  excludeNotDocumented: true,
+  sourceLinkTemplate: "https://github.com/remeda/remeda/blob/main/{path}",
+};
diff --git a/docs/typedoc.json b/docs/typedoc.json
deleted file mode 100644
index b89fb75..0000000
--- a/docs/typedoc.json
+++ /dev/null
@@ -1,14 +0,0 @@
-{
-  "$schema": "https://typedoc.org/schema.json",
-
-  "tsconfig": "../tsconfig.json",
-  "entryPoints": ["../src/index.ts"],
-  "exclude": ["**/*.test.ts"],
-
-  "jsDocCompatibility": {
-    "exampleTag": false
-  },
-
-  "excludeNotDocumented": true,
-  "sourceLinkTemplate": "https://github.com/remeda/remeda/blob/main/{path}"
-}
diff --git a/src/doNothing.ts b/src/doNothing.ts
index aff49ae..6c64431 100644
--- a/src/doNothing.ts
+++ b/src/doNothing.ts
@@ -20,12 +20,13 @@
  * @dataLast
  * @category Function
  */
-export const doNothing = (): typeof doesNothing =>
+export function doNothing(): typeof doesNothing {
   // Notice that the exported identity function is just the "factory" for the
   // function. We do it this way so that all "Function" utilities have a similar
   // API where the function is called, and not just used "headless". e.g.
   // `doNothing()` and not `doNothing`, just like the API for `constant(1)`.
-  doesNothing;
+  return doesNothing;
+}
 
 // eslint-disable-next-line @typescript-eslint/no-unnecessary-type-parameters -- There is no other way to make typescript infer the function arguments "backwards" in data-last invocations without the Args type parameter. @see: https://github.com/typescript-eslint/typescript-eslint/issues/9887
 function doesNothing<Args extends ReadonlyArray<unknown>>(
diff --git a/src/fromEntries.ts b/src/fromEntries.ts
index 327c92b..50e6155 100644
--- a/src/fromEntries.ts
+++ b/src/fromEntries.ts
@@ -1,7 +1,12 @@
 import type { Simplify } from "type-fest";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer, RemedaTypeError } from "./internal/types";
 import { purry } from "./purry";
 
+type FromEntriesError<Message extends string> = RemedaTypeError<
+  "fromEntries",
+  Message
+>;
+
 type Entry<Key extends PropertyKey = PropertyKey, Value = unknown> = readonly [
   key: Key,
   value: Value,
@@ -23,14 +28,14 @@ type FromEntries<Entries> = Entries extends readonly [
     ? FromEntriesTuple<Last, Head>
     : Entries extends IterableContainer<Entry>
       ? FromEntriesArray<Entries>
-      : "ERROR: Entries array-like could not be inferred";
+      : FromEntriesError<"Entries array-like could not be inferred">;
 
 // For strict tuples we build the result by intersecting each entry as a record
 // between it's key and value, recursively. The recursion goes through our main
 // type so that we support tuples which also contain rest parts.
 type FromEntriesTuple<E, Rest> = E extends Entry
   ? FromEntries<Rest> & Record<E[0], E[1]>
-  : "ERROR: Array-like contains a non-entry element";
+  : FromEntriesError<"Array-like contains a non-entry element">;
 
 // For the array case we also need to handle what kind of keys it defines:
 // 1. If it defines a *broad* key (one that has an infinite set of values, like
diff --git a/src/identity.ts b/src/identity.ts
index 368ad6f..1fb521c 100644
--- a/src/identity.ts
+++ b/src/identity.ts
@@ -17,12 +17,13 @@ type IdentityFunction = <T>(firstParameter: T, ...rest: any) => T;
  *    R.map([1,2,3], R.identity()); // => [1,2,3]
  * @category Function
  */
-export const identity = (): IdentityFunction =>
+export function identity(): IdentityFunction {
   // Notice that the exported identity function is just the "factory" for the
   // identity function. We do it this way so that all "Function" utilities have
   // a similar API where the function is called, and not just used "headless".
   // e.g. `identity()` and not `identity`, just like the API for `constant(1)`.
-  identityImplementation;
+  return identityImplementation;
+}
 
 const identityImplementation: IdentityFunction = (firstParameter) =>
   firstParameter;
diff --git a/src/index.ts b/src/index.ts
index a7e5a97..34c3d33 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -89,6 +89,8 @@ export * from "./omit";
 export * from "./omitBy";
 export * from "./once";
 export * from "./only";
+export * from "./partialBind";
+export * from "./partialLastBind";
 export * from "./partition";
 export * from "./pathOr";
 export * from "./pick";
diff --git a/src/internal/types.ts b/src/internal/types.ts
index 369f368..d4237f3 100644
--- a/src/internal/types.ts
+++ b/src/internal/types.ts
@@ -1,5 +1,6 @@
 import type {
   EmptyObject,
+  IsEqual,
   IfNever,
   IsAny,
   IsLiteral,
@@ -13,6 +14,27 @@ import type {
   Tagged,
 } from "type-fest";
 
+/* v8 ignore next 2 */
+// eslint-disable-next-line @typescript-eslint/no-unused-vars -- This symbol should only be used for RemedaTypeError
+const RemedaErrorSymbol = Symbol("RemedaError");
+
+/**
+ * Used for reporting type errors in a more useful way than `never`. Use
+ * numbers for things that should never happen.
+ */
+export type RemedaTypeError<
+  FunctionName extends string,
+  Message extends string | number,
+> = Message extends string
+  ? Tagged<
+      typeof RemedaErrorSymbol,
+      `RemedaTypeError(${FunctionName}): ${Message}.`
+    >
+  : RemedaTypeError<
+      FunctionName,
+      `Internal error ${Message}. Please open a Remeda GitHub issue.`
+    >;
+
 // eslint-disable-next-line @typescript-eslint/no-unused-vars -- We want to confine the typing to a specific symbol
 declare const TAG_NAME_BRANDED_RETURN: unique symbol;
 
@@ -255,9 +277,9 @@ export type NTuple<
 > = Result["length"] extends N ? Result : NTuple<T, N, [...Result, T]>;
 
 /**
- * Takes an array and returns the types that make up it's parts. The suffix is
- * anything before the rest parameter (if any), the prefix is anything after the
- * rest parameter (if any), and the item is the type of the rest parameter.
+ * Takes an array and returns the types that make up its parts. The prefix is
+ * anything before the rest parameter (if any), the suffix is anything after
+ * the rest parameter (if any), and the item is the type of the rest parameter.
  *
  * The output could be used to reconstruct the input: `[
  *   ...TupleParts<T>["prefix"],
@@ -273,13 +295,66 @@ export type TupleParts<
   ? TupleParts<Tail, [...Prefix, Head], Suffix>
   : T extends readonly [...infer Head, infer Tail]
     ? TupleParts<Head, Prefix, [Tail, ...Suffix]>
-    : T extends ReadonlyArray<infer Item>
-      ? {
-          prefix: Prefix;
-          item: Item;
-          suffix: Suffix;
-        }
-      : never;
+    : // We need to distinguish between e.g. [number? ...Array<string>] and
+      // Array<number | string>.
+      IsTupleRestOnly<T> extends false
+      ? // This is the [number? ...Array<string>] case.
+        T extends readonly [(infer MaybeHead)?, ...infer Tail]
+        ? TupleParts<Tail, [...Prefix, MaybeHead?], Suffix>
+        : never
+      : // This is the Array<number | string> case.
+        T extends ReadonlyArray<infer Item>
+        ? {
+            prefix: Prefix;
+            item: Item;
+            suffix: Suffix;
+          }
+        : never;
+/** Helper type for `TupleParts`. Checks if T = ReadonlyArray<U> for some U. */
+type IsTupleRestOnly<T> = T extends readonly []
+  ? true
+  : T extends readonly [unknown?, ...infer Tail]
+    ? IsEqual<Readonly<T>, Readonly<Tail>>
+    : false;
+
+/** The union of all possible ways to write a tuple as [...left, ...right]. */
+export type TupleSplits<Tuple extends IterableContainer> =
+  // Use a distributive conditional type, in case T is a union:
+  Tuple extends infer T
+    ? TupleParts<T> extends {
+        prefix: infer Prefix extends ReadonlyArray<unknown>;
+        item: infer Item;
+        suffix: infer Suffix extends ReadonlyArray<unknown>;
+      }
+      ? // Three cases: split is in the prefix, in the item, or in the suffix.
+        | FixedTupleSplits<Prefix, [...CoercedArray<Item>, ...Suffix]>
+          | {
+              left: [...Prefix, ...CoercedArray<Item>];
+              right: [...CoercedArray<Item>, ...Suffix];
+            }
+          | (FixedTupleSplits<Suffix> extends infer U
+              ? U extends {
+                  left: infer L extends ReadonlyArray<unknown>;
+                  right: infer R;
+                }
+                ? { left: [...Prefix, ...CoercedArray<Item>, ...L]; right: R }
+                : never
+              : never)
+      : never
+    : never;
+/** Helper type for `TupleSplits`, for tuples without rest params. */
+type FixedTupleSplits<
+  L extends IterableContainer,
+  R extends IterableContainer = [],
+> =
+  | { left: L; right: R }
+  | (L extends readonly []
+      ? never
+      : L extends readonly [...infer LHead, infer LTail]
+        ? FixedTupleSplits<LHead, [LTail, ...R]>
+        : L extends readonly [...infer LHead, (infer LTail)?]
+          ? FixedTupleSplits<LHead, [LTail?, ...R]>
+          : never);
 
 /**
  * `never[]` and `[]` are not the same type, and in some cases they aren't
diff --git a/src/partialBind.ts b/src/partialBind.ts
new file mode 100644
index 0000000..aa28f7a
--- /dev/null
+++ b/src/partialBind.ts
@@ -0,0 +1,77 @@
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return --
+ * Function inference doesn't work when `unknown` is used as the parameters
+ * generic type, it **has** to be `any`.
+ */
+import type {
+  IterableContainer,
+  RemedaTypeError,
+  TupleSplits,
+} from "./internal/types";
+
+type PartialBindError<Message extends string | number> = RemedaTypeError<
+  "partialBind",
+  Message
+>;
+
+type TuplePrefix<T extends IterableContainer> = TupleSplits<T>["left"];
+
+type RemovePrefix<
+  T extends IterableContainer,
+  Prefix extends TuplePrefix<T>,
+> = Prefix extends readonly []
+  ? T
+  : T extends readonly [infer THead, ...infer TRest]
+    ? Prefix extends readonly [infer _PrefixHead, ...infer PrefixRest]
+      ? // PrefixHead extends THead.
+        RemovePrefix<TRest, PrefixRest>
+      : // Prefix (as a whole) extends ReadonlyArray<THead>.
+        // Prefix could possibly be empty, so this has to be THead?.
+        [THead?, ...RemovePrefix<TRest, Prefix>]
+    : // T has an optional or rest parameter last. If T is a parameter list,
+      // this can only happen if we have optional arguments or a rest param;
+      // both cases are similar.
+      T extends readonly [(infer _THead)?, ...infer TRest]
+      ? Prefix extends readonly [infer _PrefixHead, ...infer PrefixRest]
+        ? // PrefixHead extends THead.
+          RemovePrefix<TRest, PrefixRest>
+        : // Prefix (as a whole) extends [THead?, ...TRest].
+          TRest
+      : // We got passed a parameter list that isn't what we expected; this is
+        // an internal error.
+        PartialBindError<1>;
+
+/**
+ * Creates a function that calls `func` with `partial` put before the arguments
+ * it receives.
+ *
+ * Can be thought of as "freezing" some portion of a function's arguments,
+ * resulting in a new function with a simplified signature.
+ *
+ * @param func - The function to wrap.
+ * @param partial - The arguments to put before.
+ * @returns A partially bound function.
+ * @signature
+ *    R.partialBind(func, partial)
+ * @example
+ *    const fn = (x, y, z) => `${x}, ${y}, and ${z}`
+ *    const partialFn = R.partialBind(fn, [1, 2])
+ *    partialFn(3) // => 1, 2, and 3
+ *
+ *    const logWithPrefix = R.partialBind(console.log, ["[prefix]"])
+ *    logWithPrefix("hello") // => "[prefix] hello"
+ * @dataFirst
+ * @category Function
+ * @see partialLastBind
+ */
+export function partialBind<
+  F extends (...args: any) => any,
+  PrefixArgs extends TuplePrefix<Parameters<F>>,
+  RemovedPrefix extends RemovePrefix<Parameters<F>, PrefixArgs>,
+>(
+  func: F,
+  ...partial: PrefixArgs
+): (
+  ...rest: RemovedPrefix extends IterableContainer ? RemovedPrefix : never
+) => ReturnType<F> {
+  return (...rest) => func(...partial, ...rest);
+}
diff --git a/src/partialLastBind.ts b/src/partialLastBind.ts
new file mode 100644
index 0000000..285e34b
--- /dev/null
+++ b/src/partialLastBind.ts
@@ -0,0 +1,86 @@
+/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/no-unsafe-return --
+ * Function inference doesn't work when `unknown` is used as the parameters
+ * generic type, it **has** to be `any`.
+ */
+import type {
+  IterableContainer,
+  RemedaTypeError,
+  TupleSplits,
+} from "./internal/types";
+
+type PartialLastBindError<Message extends string | number> = RemedaTypeError<
+  "partialLastBind",
+  Message
+>;
+
+type TupleSuffix<T extends IterableContainer> = TupleSplits<T>["right"];
+
+type RemoveSuffix<
+  T extends IterableContainer,
+  Suffix extends TupleSuffix<T>,
+> = Suffix extends readonly []
+  ? T
+  : T extends readonly [...infer TRest, infer TLast]
+    ? Suffix extends readonly [...infer SuffixRest, infer _SuffixLast]
+      ? // SuffixLast extends TLast.
+        RemoveSuffix<TRest, SuffixRest>
+      : // Suffix (as a whole) extends ReadonlyArray<TLast>.
+        // Suffix could possibly be empty, so this has to be TLast?.
+        [...RemoveSuffix<TRest, Suffix>, TLast?]
+    : // T has an optional or rest parameter last. If T is a parameter list,
+      // this can only happen if we have optional arguments or a rest param;
+      // both cases are similar.
+      T extends readonly [...infer TRest, (infer _TLast)?]
+      ? Suffix extends readonly [...infer SuffixRest, infer _SuffixLast]
+        ? // SuffixLast extends TLast.
+          RemoveSuffix<TRest, SuffixRest>
+        : // Suffix (as a whole) extends [...TRest, TLast?].
+          TRest
+      : // We got passed a parameter list that isn't what we expected; this
+        // is an internal error.
+        PartialLastBindError<1>;
+
+/**
+ * Creates a function that calls `func` with `partial` put after the arguments
+ * it receives. Note that this doesn't support functions with both optional
+ * and rest parameters.
+ *
+ * Can be thought of as "freezing" some portion of a function's arguments,
+ * resulting in a new function with a simplified signature.
+ *
+ * Useful for converting a data-first function to a data-last one.
+ *
+ * @param func - The function to wrap.
+ * @param partial - The arguments to put after.
+ * @returns A partially bound function.
+ * @signature
+ *    R.partialLastBind(func, partial)
+ * @example
+ *    const fn = (x, y, z) => `${x}, ${y}, and ${z}`
+ *    const partialFn = R.partialLastBind(fn, [2, 3])
+ *    partialFn(1) // => 1, 2, and 3
+ *
+ *    const parseBinary = R.partialLastBind(parseInt, ["2"])
+ *    parseBinary("101") // => 5
+ *
+ *    R.pipe(
+ *      { a: 1 },
+ *      // instead of (arg) => JSON.stringify(arg, null, 2)
+ *      R.partialLastBind(JSON.stringify, [null, 2]),
+ *    ) // => '{\n  "a": 1\n}'
+ * @dataFirst
+ * @category Function
+ * @see partialBind
+ */
+export function partialLastBind<
+  F extends (...args: any) => any,
+  SuffixArgs extends TupleSuffix<Parameters<F>>,
+  RemovedSuffix extends RemoveSuffix<Parameters<F>, SuffixArgs>,
+>(
+  func: F,
+  ...partial: SuffixArgs
+): (
+  ...rest: RemovedSuffix extends IterableContainer ? RemovedSuffix : never
+) => ReturnType<F> {
+  return (...rest) => func(...rest, ...partial);
+}
diff --git a/src/prop.ts b/src/prop.ts
index 0257974..3857fbb 100644
--- a/src/prop.ts
+++ b/src/prop.ts
@@ -31,7 +31,5 @@ export function prop(...args: ReadonlyArray<unknown>): unknown {
   return purry(propImplementation, args);
 }
 
-export const propImplementation = <T, K extends keyof T>(
-  data: T,
-  key: K,
-): T[K] => data[key];
+const propImplementation = <T, K extends keyof T>(data: T, key: K): T[K] =>
+  data[key];


Test patch:
diff --git a/src/internal/types.test-d.ts b/src/internal/types.test-d.ts
index 91b7937..eec5a01 100644
--- a/src/internal/types.test-d.ts
+++ b/src/internal/types.test-d.ts
@@ -7,6 +7,7 @@ import type {
   IterableContainer,
   NTuple,
   TupleParts,
+  TupleSplits,
 } from "./types";
 
 declare const SymbolFoo: unique symbol;
@@ -384,6 +385,16 @@ describe("TupleParts", () => {
         suffix: [string];
       }>();
     });
+
+    test("array with optional prefixes", () => {
+      const data = [1, "a"] as [number, string?, number?, ...Array<boolean>];
+      const result = tupleParts(data);
+      expectTypeOf(result).toEqualTypeOf<{
+        prefix: [number, string?, number?];
+        item: boolean;
+        suffix: [];
+      }>();
+    });
   });
 
   describe("readonly", () => {
@@ -446,6 +457,21 @@ describe("TupleParts", () => {
         suffix: [string];
       }>();
     });
+
+    test("array with optional prefixes", () => {
+      const data = [1, "a"] as readonly [
+        number,
+        string?,
+        number?,
+        ...Array<boolean>,
+      ];
+      const result = tupleParts(data);
+      expectTypeOf(result).toEqualTypeOf<{
+        prefix: [number, string?, number?];
+        item: boolean;
+        suffix: [];
+      }>();
+    });
   });
 
   describe("unions", () => {
@@ -469,6 +495,229 @@ describe("TupleParts", () => {
   });
 });
 
+describe("TupleSplits", () => {
+  describe("mutable", () => {
+    test("empty array", () => {
+      const data = [] as [];
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<{
+        left: [];
+        right: [];
+      }>();
+    });
+
+    test("regular array", () => {
+      const data = [] as Array<number>;
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [];
+            right: Array<number>;
+          }
+        | {
+            left: Array<number>;
+            right: Array<number>;
+          }
+        | {
+            left: Array<number>;
+            right: [];
+          }
+      >();
+    });
+
+    test("fixed tuple", () => {
+      const data = [1, 2, 3] as [1, 2, 3];
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [1, 2, 3];
+            right: [];
+          }
+        | {
+            left: [1, 2];
+            right: [3];
+          }
+        | {
+            left: [1];
+            right: [2, 3];
+          }
+        | {
+            left: [];
+            right: [1, 2, 3];
+          }
+      >();
+    });
+
+    test("array with prefix", () => {
+      const data = ["a"] as [string, ...Array<boolean>];
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [];
+            right: [string, ...Array<boolean>];
+          }
+        | {
+            left: [string];
+            right: Array<boolean>;
+          }
+        | {
+            left: [string, ...Array<boolean>];
+            right: Array<boolean>;
+          }
+        | {
+            left: [string, ...Array<boolean>];
+            right: [];
+          }
+      >();
+    });
+
+    test("array with suffix", () => {
+      const data = ["a"] as [...Array<boolean>, string];
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [];
+            right: [...Array<boolean>, string];
+          }
+        | {
+            left: Array<boolean>;
+            right: [...Array<boolean>, string];
+          }
+        | {
+            left: Array<boolean>;
+            right: [string];
+          }
+        | {
+            left: [...Array<boolean>, string];
+            right: [];
+          }
+      >();
+    });
+
+    test("array with prefix and suffix", () => {
+      const data = [1, "a"] as [number, ...Array<boolean>, string];
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [];
+            right: [number, ...Array<boolean>, string];
+          }
+        | {
+            left: [number];
+            right: [...Array<boolean>, string];
+          }
+        | {
+            left: [number, ...Array<boolean>];
+            right: [...Array<boolean>, string];
+          }
+        | {
+            left: [number, ...Array<boolean>];
+            right: [string];
+          }
+        | {
+            left: [number, ...Array<boolean>, string];
+            right: [];
+          }
+      >();
+    });
+
+    test("array with optional prefixes", () => {
+      const data = [1, "a"] as [number, string?, number?, ...Array<boolean>];
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [];
+            right: [number, string?, number?, ...Array<boolean>];
+          }
+        | {
+            left: [number];
+            right: [string?, number?, ...Array<boolean>];
+          }
+        | {
+            left: [number, string?];
+            right: [number?, ...Array<boolean>];
+          }
+        | {
+            left: [number, string?, number?];
+            right: [...Array<boolean>];
+          }
+        | {
+            left: [number, string?, number?, ...Array<boolean>];
+            right: Array<boolean>;
+          }
+        | {
+            left: [number, string?, number?, ...Array<boolean>];
+            right: [];
+          }
+      >();
+    });
+  });
+
+  describe("unions", () => {
+    test("union of arrays", () => {
+      const data = [] as Array<boolean> | Array<number>;
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [];
+            right: Array<boolean>;
+          }
+        | {
+            left: Array<boolean>;
+            right: Array<boolean>;
+          }
+        | {
+            left: Array<boolean>;
+            right: [];
+          }
+        | {
+            left: [];
+            right: Array<number>;
+          }
+        | {
+            left: Array<number>;
+            right: Array<number>;
+          }
+        | {
+            left: Array<number>;
+            right: [];
+          }
+      >();
+    });
+
+    test("mixed unions", () => {
+      const data = [] as Array<boolean> | [number, string];
+      const result = tupleSplits(data);
+      expectTypeOf(result).toEqualTypeOf<
+        | {
+            left: [];
+            right: Array<boolean>;
+          }
+        | {
+            left: Array<boolean>;
+            right: Array<boolean>;
+          }
+        | {
+            left: Array<boolean>;
+            right: [];
+          }
+        | {
+            left: [];
+            right: [number, string];
+          }
+        | {
+            left: [number];
+            right: [string];
+          }
+        | {
+            left: [number, string];
+            right: [];
+          }
+      >();
+    });
+  });
+});
+
 describe("DeDupped", () => {
   describe("mutable", () => {
     test("empty array", () => {
@@ -568,4 +817,6 @@ declare function nTuple<T, N extends number>(x: T, n: N): NTuple<T, N>;
 
 declare function tupleParts<T>(x: T): TupleParts<T>;
 
+declare function tupleSplits<T extends IterableContainer>(x: T): TupleSplits<T>;
+
 declare function deduped<T extends IterableContainer>(data: T): Deduped<T>;
diff --git a/src/partialBind.test-d.ts b/src/partialBind.test-d.ts
new file mode 100644
index 0000000..ebca6cb
--- /dev/null
+++ b/src/partialBind.test-d.ts
@@ -0,0 +1,190 @@
+/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types, unicorn/consistent-function-scoping */
+import { partialBind } from "./partialBind";
+
+describe("simple case (all required, no rest params)", () => {
+  const fn = (x: number, y: number, z: number | string): string =>
+    `${x}, ${y}, and ${z}`;
+
+  test("should correctly type 0 partial args", () => {
+    expectTypeOf(partialBind(fn)).toEqualTypeOf<
+      (x: number, y: number, z: number | string) => string
+    >();
+  });
+
+  test("should correctly type 1 partial arg", () => {
+    expectTypeOf(partialBind(fn, 1)).toEqualTypeOf<
+      (y: number, z: number | string) => string
+    >();
+  });
+
+  test("should correctly type all partial args", () => {
+    expectTypeOf(partialBind(fn, 1, 2, "a")).toEqualTypeOf<() => string>();
+  });
+
+  test("should not accept wrong arg type", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialBind(fn, "a");
+  });
+
+  test("should not accept too many args", () => {
+    // @ts-expect-error [ts2345] - too many args
+    partialBind(fn, 1, 2, 3, 4);
+  });
+
+  test("should not accept array typed partial", () => {
+    // @ts-expect-error [ts2345] - don't know how many args are bound
+    partialBind(fn, ...([] as Array<number>));
+  });
+
+  test("should not accept tuple typed partial with suffix", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialBind(fn, ...([1, "a"] as [...Array<number>, string]));
+  });
+});
+
+describe("optional params", () => {
+  const fn = (x: string, y = 123, z = true): string => `${x}, ${y}, and ${z}`;
+
+  test("should correctly type 0 partial args", () => {
+    expectTypeOf(partialBind(fn)).toEqualTypeOf<
+      (x: string, y?: number, z?: boolean) => string
+    >();
+  });
+
+  test("should correctly type 1 partial arg", () => {
+    expectTypeOf(partialBind(fn, "hello")).toEqualTypeOf<
+      (y?: number, z?: boolean) => string
+    >();
+  });
+
+  test("should correctly type 2 partial args", () => {
+    expectTypeOf(partialBind(fn, "hello", undefined)).toEqualTypeOf<
+      (z?: boolean) => string
+    >();
+  });
+
+  test("should correctly type all partial args", () => {
+    expectTypeOf(partialBind(fn, "hello", undefined, false)).toEqualTypeOf<
+      () => string
+    >();
+  });
+});
+
+describe("simple rest param case", () => {
+  const fn = (...parts: Array<string>): string => parts.join("");
+
+  test("should correctly type 0 partial args", () => {
+    expectTypeOf(partialBind(fn)).toEqualTypeOf<
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should correctly type 1 partial arg", () => {
+    expectTypeOf(partialBind(fn, "hello")).toEqualTypeOf<
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should not accept wrong arg type", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialBind(fn, 1);
+  });
+
+  test("should accept tuple typed partial arg", () => {
+    expectTypeOf(partialBind(fn, ...([] as Array<string>))).toEqualTypeOf<
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should accept tuple typed partial arg with prefix", () => {
+    expectTypeOf(
+      partialBind(fn, ...(["hello"] as [string, ...Array<string>])),
+    ).toEqualTypeOf<(...parts: ReadonlyArray<string>) => string>();
+  });
+
+  test("should accept tuple typed partial arg with prefix and suffix", () => {
+    expectTypeOf(
+      partialBind(
+        fn,
+        ...(["hello", "world"] as [string, ...Array<string>, string]),
+      ),
+    ).toEqualTypeOf<(...parts: ReadonlyArray<string>) => string>();
+  });
+
+  test("should not accept tuple typed partial arg with incorrect prefix", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialBind(fn, ...([1, "hello"] as [number?, ...Array<string>]));
+  });
+
+  test("should not accept tuple typed partial arg with incorrect suffix", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialBind(fn, ...(["hello", 1] as [...Array<string>, number]));
+  });
+});
+
+describe("optional and rest param case", () => {
+  const fn = (x: string, y = 123, ...parts: Array<string>): string =>
+    `${x}, ${y}, and ${parts.join("")}`;
+
+  test("should correctly type 0 partial args", () => {
+    expectTypeOf(partialBind(fn)).toEqualTypeOf<
+      (x: string, y?: number, ...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should correctly type 1 partial arg", () => {
+    expectTypeOf(partialBind(fn, "hello")).toEqualTypeOf<
+      (y?: number, ...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should correctly type 2 partial args", () => {
+    expectTypeOf(partialBind(fn, "hello", undefined)).toEqualTypeOf<
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should correctly type 3 partial args", () => {
+    expectTypeOf(partialBind(fn, "hello", undefined, "world")).toEqualTypeOf<
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should not accept wrong required arg type", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialBind(fn, 1);
+  });
+
+  test("should not accept wrong optional arg type", () => {
+    // @ts-expect-error [ts2345] - This is correct; "hello" would be the second argument of fn.
+    partialBind(fn, "hello", "world");
+  });
+
+  test("should not accept wrong rest arg type", () => {
+    // @ts-expect-error [ts2345] - wrong rest arg type
+    partialBind(fn, "hello", 123, 1);
+  });
+
+  test("should not accept incorrect tuple typed partial arg", () => {
+    // @ts-expect-error [ts2345] - doesn't match optional
+    partialBind(fn, ...([] as Array<string>));
+  });
+
+  test("should accept correct tuple typed partial arg", () => {
+    expectTypeOf(
+      partialBind(fn, ...(["hello"] as [string, number?, ...Array<string>])),
+    ).toEqualTypeOf<(...parts: ReadonlyArray<string>) => string>();
+  });
+});
+
+describe("KNOWN ISSUES", () => {
+  test("does not support readonly rest params", () => {
+    const fn = (...parts: ReadonlyArray<string>): string => parts.join("");
+
+    // @ts-expect-error [ts2345]: blocked on https://github.com/microsoft/TypeScript/issues/37193
+    expectTypeOf(partialBind(fn)).toEqualTypeOf<
+      // @ts-expect-error [ts2344]: blocked on https://github.com/microsoft/TypeScript/issues/37193
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+});
diff --git a/src/partialBind.test.ts b/src/partialBind.test.ts
new file mode 100644
index 0000000..0d61cc2
--- /dev/null
+++ b/src/partialBind.test.ts
@@ -0,0 +1,15 @@
+import { partialBind } from "./partialBind";
+
+const fn = (x: number, y: number, z: number): string => `${x}, ${y}, and ${z}`;
+
+test("should partially apply 0 args", () => {
+  expect(partialBind(fn)(1, 2, 3)).toBe(fn(1, 2, 3));
+});
+
+test("should partially apply 1 arg", () => {
+  expect(partialBind(fn, 1)(2, 3)).toBe(fn(1, 2, 3));
+});
+
+test("should partially apply all args", () => {
+  expect(partialBind(fn, 1, 2, 3)()).toBe(fn(1, 2, 3));
+});
diff --git a/src/partialLastBind.test-d.ts b/src/partialLastBind.test-d.ts
new file mode 100644
index 0000000..2d056ed
--- /dev/null
+++ b/src/partialLastBind.test-d.ts
@@ -0,0 +1,146 @@
+/* eslint-disable @typescript-eslint/prefer-readonly-parameter-types, unicorn/consistent-function-scoping */
+import { partialLastBind } from "./partialLastBind";
+
+describe("simple case (all required, no rest params)", () => {
+  const fn = (x: number, y: number, z: number | string): string =>
+    `${x}, ${y}, and ${z}`;
+
+  test("should correctly type 0 partial args", () => {
+    expectTypeOf(partialLastBind(fn)).toEqualTypeOf<
+      (x: number, y: number, z: number | string) => string
+    >();
+  });
+
+  test("should correctly type 1 partial arg", () => {
+    expectTypeOf(partialLastBind(fn, 3)).toEqualTypeOf<
+      (x: number, y: number) => string
+    >();
+  });
+
+  test("should correctly type all partial args", () => {
+    expectTypeOf(partialLastBind(fn, 1, 2, "c")).toEqualTypeOf<() => string>();
+  });
+
+  test("should not accept wrong arg type", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialLastBind(fn, "b", "c");
+  });
+
+  test("should not accept too many args", () => {
+    // @ts-expect-error [ts2345] - too many args
+    partialLastBind(fn, 1, 2, 3, 4);
+  });
+
+  test("should not accept array typed partial", () => {
+    // @ts-expect-error [ts2345] - don't know how many args are bound
+    partialLastBind(fn, ...([] as Array<number>));
+  });
+
+  test("should not accept tuple typed partial with prefix", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialLastBind(fn, ...(["a", 1] as [string, ...Array<number>]));
+  });
+});
+
+describe("optional params", () => {
+  const fn = (x: string, y = 123, z = true): string => `${x}, ${y}, and ${z}`;
+
+  test("should correctly type 0 partial args", () => {
+    expectTypeOf(partialLastBind(fn)).toEqualTypeOf<
+      (x: string, y?: number, z?: boolean) => string
+    >();
+  });
+
+  test("should correctly type 1 partial arg", () => {
+    expectTypeOf(partialLastBind(fn, false)).toEqualTypeOf<
+      (x: string, y?: number) => string
+    >();
+  });
+
+  test("should correctly type 2 partial args", () => {
+    expectTypeOf(partialLastBind(fn, undefined, false)).toEqualTypeOf<
+      (x: string) => string
+    >();
+  });
+
+  test("should correctly type all partial args", () => {
+    expectTypeOf(partialLastBind(fn, "hello", undefined, false)).toEqualTypeOf<
+      () => string
+    >();
+  });
+});
+
+describe("simple rest param case", () => {
+  const fn = (...parts: Array<string>): string => parts.join("");
+
+  test("should correctly type 0 partial args", () => {
+    expectTypeOf(partialLastBind(fn)).toEqualTypeOf<
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should correctly type 1 partial arg", () => {
+    expectTypeOf(partialLastBind(fn, "hello")).toEqualTypeOf<
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  test("should not accept wrong arg type", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialLastBind(fn, 1);
+  });
+
+  test("should accept tuple typed partial arg", () => {
+    expectTypeOf(
+      partialLastBind(fn, ...([] as [...Array<string>])),
+    ).toEqualTypeOf<(...parts: ReadonlyArray<string>) => string>();
+  });
+
+  test("should accept tuple typed partial arg with prefix", () => {
+    expectTypeOf(
+      partialLastBind(fn, ...(["hello"] as [string, ...Array<string>])),
+    ).toEqualTypeOf<(...parts: ReadonlyArray<string>) => string>();
+  });
+
+  test("should accept tuple typed partial arg with prefix and suffix", () => {
+    expectTypeOf(
+      partialLastBind(
+        fn,
+        ...(["hello", "world"] as [string, ...Array<string>, string]),
+      ),
+    ).toEqualTypeOf<(...parts: ReadonlyArray<string>) => string>();
+  });
+
+  test("should not accept tuple typed partial arg with incorrect prefix", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialLastBind(fn, ...([1, "hello"] as [number?, ...Array<string>]));
+  });
+
+  test("should not accept tuple typed partial arg with incorrect suffix", () => {
+    // @ts-expect-error [ts2345] - wrong arg type
+    partialLastBind(fn, ...(["hello", 1] as [...Array<string>, number]));
+  });
+});
+
+describe("KNOWN ISSUES", () => {
+  test("does not support readonly rest params", () => {
+    const fn = (...parts: ReadonlyArray<string>): string => parts.join("");
+
+    // @ts-expect-error [ts2345]: blocked on https://github.com/microsoft/TypeScript/issues/37193
+    expectTypeOf(partialLastBind(fn)).toEqualTypeOf<
+      // @ts-expect-error [ts2344]: blocked on https://github.com/microsoft/TypeScript/issues/37193
+      (...parts: ReadonlyArray<string>) => string
+    >();
+  });
+
+  describe("does not support optional AND rest params", () => {
+    const fn = (x: string, y = 123, ...parts: Array<string>): string =>
+      `${x}, ${y}, and ${parts.join("")}`;
+
+    expectTypeOf(partialLastBind(fn, "hello")).toEqualTypeOf<
+      // @ts-expect-error [ts2344]: I don't think this is possible on the type-level?
+      // We don't know whether "hello" is in x or parts.
+      (x: string, y?: number, ...parts: ReadonlyArray<string>) => string
+    >();
+  });
+});
diff --git a/src/partialLastBind.test.ts b/src/partialLastBind.test.ts
new file mode 100644
index 0000000..1fe7c56
--- /dev/null
+++ b/src/partialLastBind.test.ts
@@ -0,0 +1,15 @@
+import { partialLastBind } from "./partialLastBind";
+
+const fn = (x: number, y: number, z: number): string => `${x}, ${y}, and ${z}`;
+
+test("should partially apply 0 args", () => {
+  expect(partialLastBind(fn)(1, 2, 3)).toBe(fn(1, 2, 3));
+});
+
+test("should partially apply 1 arg", () => {
+  expect(partialLastBind(fn, 3)(1, 2)).toBe(fn(1, 2, 3));
+});
+
+test("should partially apply all args", () => {
+  expect(partialLastBind(fn, 1, 2, 3)()).toBe(fn(1, 2, 3));
+});