Repository: remeda/remeda
Pull request #915 (2024-11-06 11:41:05)

chore: split internal/types.ts
The idea of splitting the file came up recently in a PR. I think it makes sense, as the file has grown bigger and is harder to maintain.

This also makes testing easier and makes gaps in our type tests more apparent. 

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/915/builds/558030) or the icon next to each commit SHA.

Latest deployment of this branch, based on commit f38bc155f0dbd6fe8b09febf7f04b9ea5dbc501b:

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

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remeda/remeda/pull/915?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 [(`1d50167`)](https://app.codecov.io/gh/remeda/remeda/commit/1d50167b54e50636a8fd711decc84d7508ca64db?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) to head [(`f38bc15`)](https://app.codecov.io/gh/remeda/remeda/commit/f38bc155f0dbd6fe8b09febf7f04b9ea5dbc501b?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      #915   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files          323       343   +20     
  Lines        10884     10904   +20     
  Branches      2746      2766   +20     
=========================================
+ Hits         10884     10904   +20     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/remeda/remeda/pull/915?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/docs/src/content/mapping/lodash/__MISSING.md b/docs/src/content/mapping/lodash/__MISSING.md
index 08f2234..b679d97 100644
--- a/docs/src/content/mapping/lodash/__MISSING.md
+++ b/docs/src/content/mapping/lodash/__MISSING.md
@@ -129,7 +129,6 @@ TODO: Go over: https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#
 - deburr
 - escape
 - escapeRegExp
-- kebabCase
 - lowerCase
 - pad
 - padEnd
@@ -137,7 +136,6 @@ TODO: Go over: https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#
 - parseInt
 - repeat
 - replace
-- snakeCase
 - startCase
 - template
 - trim
diff --git a/docs/src/content/mapping/lodash/kebabCase.md b/docs/src/content/mapping/lodash/kebabCase.md
new file mode 100644
index 0000000..c7c573b
--- /dev/null
+++ b/docs/src/content/mapping/lodash/kebabCase.md
@@ -0,0 +1,7 @@
+---
+category: String
+remeda: toKebabCase
+---
+
+The remeda implementation doesn't perform deburring (changing of accented latin
+characters to simple LATIN-1 characters), and doesn't remove apostrophes.
diff --git a/docs/src/content/mapping/lodash/snakeCase.md b/docs/src/content/mapping/lodash/snakeCase.md
new file mode 100644
index 0000000..4bf0a13
--- /dev/null
+++ b/docs/src/content/mapping/lodash/snakeCase.md
@@ -0,0 +1,7 @@
+---
+category: String
+remeda: toSnakeCase
+---
+
+The remeda implementation doesn't perform deburring (changing of accented latin
+characters to simple LATIN-1 characters), and doesn't remove apostrophes.
diff --git a/eslint.config.js b/eslint.config.js
index 3b20e37..b8f1aa4 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -514,4 +514,10 @@ export default tseslint.config(
       "jsdoc/require-param": "off",
     },
   },
+  {
+    files: ["src/internal/types/*.ts"],
+    rules: {
+      "unicorn/filename-case": ["error", { case: "pascalCase" }],
+    },
+  },
 );
diff --git a/src/addProp.ts b/src/addProp.ts
index 7e2ebd4..645e9af 100644
--- a/src/addProp.ts
+++ b/src/addProp.ts
@@ -1,4 +1,4 @@
-import type { UpsertProp } from "./internal/types";
+import type { UpsertProp } from "./internal/types/UpsertProp";
 import { purry } from "./purry";
 
 /**
diff --git a/src/capitalize.ts b/src/capitalize.ts
index 4ce9b04..eaca6d9 100644
--- a/src/capitalize.ts
+++ b/src/capitalize.ts
@@ -7,7 +7,7 @@ import { purry } from "./purry";
  * utility type for typing.
  *
  * For other case manipulations see: `toUpperCase`, `toLowerCase`,
- * `uncapitalize`, and `toCamelCase`.
+ * `uncapitalize`, `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  * If the output is intended for display (on a browser) consider using
@@ -31,7 +31,7 @@ export function capitalize<T extends string>(data: T): Capitalize<T>;
  * utility type for typing.
  *
  * For other case manipulations see: `toUpperCase`, `toLowerCase`,
- * `uncapitalize`, and `toCamelCase`.
+ * `uncapitalize`, `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  * If the output is intended for display (on a browser) consider using
diff --git a/src/chunk.ts b/src/chunk.ts
index b7dce30..6436eb1 100644
--- a/src/chunk.ts
+++ b/src/chunk.ts
@@ -6,12 +6,10 @@ import type {
   Subtract,
   ValueOf,
 } from "type-fest";
-import type {
-  IterableContainer,
-  NonEmptyArray,
-  NTuple,
-  TupleParts,
-} from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
+import type { NTuple } from "./internal/types/NTuple";
+import type { TupleParts } from "./internal/types/TupleParts";
 import { purry } from "./purry";
 
 // This prevents typescript from failing on complex arrays and large chunks. It
diff --git a/src/concat.ts b/src/concat.ts
index 2345df8..222efa8 100644
--- a/src/concat.ts
+++ b/src/concat.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/conditional.ts b/src/conditional.ts
index 6790c6e..1bd9aa5 100644
--- a/src/conditional.ts
+++ b/src/conditional.ts
@@ -1,7 +1,7 @@
 /* eslint-disable jsdoc/check-param-names, jsdoc/require-param -- we don't document the case params, it'd be redundant */
 
 import { purryOn } from "./internal/purryOn";
-import type { GuardType } from "./internal/types";
+import type { GuardType } from "./internal/types/GuardType";
 
 type Case<
   In,
diff --git a/src/drop.ts b/src/drop.ts
index bbb35cf..9f4c31e 100644
--- a/src/drop.ts
+++ b/src/drop.ts
@@ -1,10 +1,8 @@
 import type { IsInteger, IsNegative, Subtract } from "type-fest";
-import type {
-  CoercedArray,
-  IterableContainer,
-  NTuple,
-  TupleParts,
-} from "./internal/types";
+import type { CoercedArray } from "./internal/types/CoercedArray";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { NTuple } from "./internal/types/NTuple";
+import type { TupleParts } from "./internal/types/TupleParts";
 import { SKIP_ITEM, lazyIdentityEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
diff --git a/src/dropFirstBy.ts b/src/dropFirstBy.ts
index 73e4fed..605fb38 100644
--- a/src/dropFirstBy.ts
+++ b/src/dropFirstBy.ts
@@ -3,7 +3,8 @@ import {
   purryOrderRulesWithArgument,
   type OrderRule,
 } from "./internal/purryOrderRules";
-import type { CompareFunction, NonEmptyArray } from "./internal/types";
+import type { CompareFunction } from "./internal/types/CompareFunction";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 /**
  * Drop the first `n` items from `data` based on the provided ordering criteria. This allows you to avoid sorting the array before dropping the items. The complexity of this function is *O(Nlogn)* where `N` is the length of the array.
diff --git a/src/dropLast.ts b/src/dropLast.ts
index 47bbc1c..31cabdf 100644
--- a/src/dropLast.ts
+++ b/src/dropLast.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/dropLastWhile.ts b/src/dropLastWhile.ts
index de76d8a..0c7fa0c 100644
--- a/src/dropLastWhile.ts
+++ b/src/dropLastWhile.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/dropWhile.ts b/src/dropWhile.ts
index ea1f5c9..59f25e4 100644
--- a/src/dropWhile.ts
+++ b/src/dropWhile.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/evolve.ts b/src/evolve.ts
index 030414c..99b0fbd 100644
--- a/src/evolve.ts
+++ b/src/evolve.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/first.ts b/src/first.ts
index 7120aa1..6215f88 100644
--- a/src/first.ts
+++ b/src/first.ts
@@ -1,5 +1,5 @@
 import { toSingle } from "./internal/toSingle";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
diff --git a/src/firstBy.ts b/src/firstBy.ts
index 1078986..f6813dc 100644
--- a/src/firstBy.ts
+++ b/src/firstBy.ts
@@ -1,10 +1,8 @@
-import { purryOrderRules, type OrderRule } from "./internal/purryOrderRules";
-import type {
-  CompareFunction,
-  IterableContainer,
-  NonEmptyArray,
-} from "./internal/types";
 import { hasAtLeast } from "./hasAtLeast";
+import { purryOrderRules, type OrderRule } from "./internal/purryOrderRules";
+import type { CompareFunction } from "./internal/types/CompareFunction";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 type FirstBy<T extends IterableContainer> =
   | T[number]
diff --git a/src/flat.ts b/src/flat.ts
index e83bd1c..e6ff6ff 100644
--- a/src/flat.ts
+++ b/src/flat.ts
@@ -1,6 +1,6 @@
 import type { IsNumericLiteral } from "type-fest";
 import { lazyDataLastImpl } from "./internal/lazyDataLastImpl";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { lazyIdentityEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator, LazyResult } from "./pipe";
 
diff --git a/src/forEach.ts b/src/forEach.ts
index acb7408..dca5c9f 100644
--- a/src/forEach.ts
+++ b/src/forEach.ts
@@ -1,5 +1,5 @@
 import type { Writable } from "type-fest";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
diff --git a/src/forEachObj.ts b/src/forEachObj.ts
index 7a5e36a..38e062d 100644
--- a/src/forEachObj.ts
+++ b/src/forEachObj.ts
@@ -1,7 +1,5 @@
-import type {
-  EnumerableStringKeyOf,
-  EnumerableStringKeyedValueOf,
-} from "./internal/types";
+import type { EnumerableStringKeyOf } from "./internal/types/EnumerableStringKeyOf";
+import type { EnumerableStringKeyedValueOf } from "./internal/types/EnumerableStringKeyedValueOf";
 import { purry } from "./purry";
 
 /**
diff --git a/src/fromEntries.ts b/src/fromEntries.ts
index 50e6155..b4d1bcc 100644
--- a/src/fromEntries.ts
+++ b/src/fromEntries.ts
@@ -1,5 +1,6 @@
 import type { Simplify } from "type-fest";
-import type { IterableContainer, RemedaTypeError } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { RemedaTypeError } from "./internal/types/RemedaTypeError";
 import { purry } from "./purry";
 
 type FromEntriesError<Message extends string> = RemedaTypeError<
diff --git a/src/fromKeys.ts b/src/fromKeys.ts
index 797f734..d51db76 100644
--- a/src/fromKeys.ts
+++ b/src/fromKeys.ts
@@ -1,5 +1,6 @@
 import type { Simplify } from "type-fest";
-import type { ExactRecord, IterableContainer } from "./internal/types";
+import type { ExactRecord } from "./internal/types/ExactRecord";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 // Takes a union of literals and creates a union of records with the value V for
diff --git a/src/groupBy.ts b/src/groupBy.ts
index bb33f08..053d61c 100644
--- a/src/groupBy.ts
+++ b/src/groupBy.ts
@@ -1,4 +1,5 @@
-import type { ExactRecord, NonEmptyArray } from "./internal/types";
+import type { ExactRecord } from "./internal/types/ExactRecord";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 import { purry } from "./purry";
 
 /**
diff --git a/src/hasAtLeast.ts b/src/hasAtLeast.ts
index 94711a5..57b22b5 100644
--- a/src/hasAtLeast.ts
+++ b/src/hasAtLeast.ts
@@ -1,5 +1,5 @@
 import type { IsNumericLiteral } from "type-fest";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type ArraySetRequired<
diff --git a/src/index.ts b/src/index.ts
index 3663fa4..5eddc2f 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -141,7 +141,9 @@ export * from "./takeWhile";
 export * from "./tap";
 export * from "./times";
 export * from "./toCamelCase";
+export * from "./toKebabCase";
 export * from "./toLowerCase";
+export * from "./toSnakeCase";
 export * from "./toUpperCase";
 export * from "./uncapitalize";
 export * from "./unique";
diff --git a/src/indexBy.ts b/src/indexBy.ts
index 4ab31d2..f5cc7d8 100644
--- a/src/indexBy.ts
+++ b/src/indexBy.ts
@@ -1,4 +1,4 @@
-import type { ExactRecord } from "./internal/types";
+import type { ExactRecord } from "./internal/types/ExactRecord";
 import { purry } from "./purry";
 
 /**
diff --git a/src/internal/heap.ts b/src/internal/heap.ts
index 6c52c63..7250dd0 100644
--- a/src/internal/heap.ts
+++ b/src/internal/heap.ts
@@ -2,9 +2,9 @@
  * Heap related utilities.
  */
 
-import { swapInPlace } from "./swapInPlace";
-import type { CompareFunction } from "./types";
 import { hasAtLeast } from "../hasAtLeast";
+import { swapInPlace } from "./swapInPlace";
+import type { CompareFunction } from "./types/CompareFunction";
 
 /**
  * Mutates an array into a "max"-heap based on `compareFn` so that for any `item` in the heap, `compareFn(heap[0], item) > 0`.
diff --git a/src/internal/purryOrderRules.ts b/src/internal/purryOrderRules.ts
index 2030409..e92aab4 100644
--- a/src/internal/purryOrderRules.ts
+++ b/src/internal/purryOrderRules.ts
@@ -1,4 +1,5 @@
-import type { CompareFunction, NonEmptyArray } from "./types";
+import type { CompareFunction } from "./types/CompareFunction";
+import type { NonEmptyArray } from "./types/NonEmptyArray";
 
 // We define the comparators in a global const so that they are only
 // instantiated once, and so we can couple a label (string) for them that could
diff --git a/src/internal/quickSelect.ts b/src/internal/quickSelect.ts
index a1d4c7a..185cd39 100644
--- a/src/internal/quickSelect.ts
+++ b/src/internal/quickSelect.ts
@@ -5,7 +5,7 @@
  */
 
 import { swapInPlace } from "./swapInPlace";
-import type { CompareFunction } from "./types";
+import type { CompareFunction } from "./types/CompareFunction";
 
 /**
  * Perform QuickSelect on the given data. Notice that the data would be cloned
diff --git a/src/internal/types.ts b/src/internal/types.ts
deleted file mode 100644
index 67f3219..0000000
--- a/src/internal/types.ts
+++ /dev/null
@@ -1,397 +0,0 @@
-import type {
-  EmptyObject,
-  IsEqual,
-  IfNever,
-  IsAny,
-  IsLiteral,
-  IsNever,
-  IsNumericLiteral,
-  IsStringLiteral,
-  IsSymbolLiteral,
-  KeysOfUnion,
-  Simplify,
-  Split,
-  Tagged,
-  Writable,
-} 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;
-
-// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The most generic function signature requires the usage of `any` instead of `unknown`
-export type BrandedReturn<F extends (...args: any) => any> = (
-  ...args: Parameters<F>
-) => Tagged<ReturnType<F>, typeof TAG_NAME_BRANDED_RETURN, F>;
-
-export type NonEmptyArray<T> = [T, ...Array<T>];
-
-export type Mapped<T extends IterableContainer, K> = {
-  -readonly [P in keyof T]: K;
-};
-
-/**
- * This should only be used for defining generics which extend any kind of JS
- * array under the hood, this includes arrays *AND* tuples (of the form [x, y],
- * and of the form [x, ...y[]], etc...), and their readonly equivalent. This
- * allows us to be more inclusive to what functions can process.
- *
- * @example map<T extends ArrayLike>(items: T) { ... }
- *
- * We would've named this `ArrayLike`, but that's already used by typescript...
- * @see This was inspired by the type-definition of Promise.all (https://github.com/microsoft/TypeScript/blob/1df5717b120cddd325deab8b0f2b2c3eecaf2b01/src/lib/es2015.promise.d.ts#L21)
- */
-export type IterableContainer<T = unknown> = ReadonlyArray<T> | readonly [];
-
-/**
- * Check if a type is guaranteed to be a bounded record: a record with a finite
- * set of keys.
- *
- * @example
- *     IfBoundedRecord<{ a: 1, 1: "a" }>; //=> true
- *     IfBoundedRecord<Record<string | number, unknown>>; //=> false
- *     IfBoundedRecord<Record<`prefix_${number}`, unknown>>; //=> false
- */
-export type IfBoundedRecord<
-  T,
-  TypeIfBoundedRecord = true,
-  TypeIfUnboundedRecord = false,
-> =
-  IsBoundedKey<KeysOfUnion<T>> extends true
-    ? TypeIfBoundedRecord
-    : TypeIfUnboundedRecord;
-
-/**
- * Checks if a type is a bounded key: a union of bounded strings, numeric
- * literals, or symbol literals.
- */
-type IsBoundedKey<T> =
-  // `extends unknown` is always going to be the case and is used to convert any
-  // union into a [distributive conditional type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types).
-  T extends unknown
-    ? IsStringLiteral<T> extends true
-      ? IsBoundedString<T>
-      : IsNumericLiteral<T> extends true
-        ? true
-        : IsSymbolLiteral<T>
-    : never;
-
-/**
- * Checks if a type is a bounded string: a type that only has a finite
- * number of strings that are that type.
- *
- * Most relevant for template literals: IsBoundedString<`${1 | 2}_${3 | 4}`> is
- * true, and IsBoundedString<`${1 | 2}_${number}`> is false.
- */
-type IsBoundedString<T> = T extends string
-  ? // Let U be the union of the types of each character in T.
-    // (T[number] alone doesn't work because that's just string.)
-    Split<T, "">[number] extends infer U
-    ? // Eliminate unbounded cases, where a character can be any number or any
-      // string. Otherwise, we assume it's bounded.
-      [`${number}`] extends [U]
-      ? false
-      : [string] extends [U]
-        ? false
-        : true
-    : false
-  : false;
-
-/**
- * A union of all keys of T which are not symbols, and where number keys are
- * converted to strings, following the definition of `Object.keys` and
- * `Object.entries`.
- *
- * Inspired and largely copied from [`sindresorhus/ts-extras`](https://github.com/sindresorhus/ts-extras/blob/44f57392c5f027268330771996c4fdf9260b22d6/source/object-keys.ts).
- *
- * @see EnumerableStringKeyedValueOf
- */
-export type EnumerableStringKeyOf<T> =
-  Required<T> extends Record<infer K, unknown>
-    ? `${Exclude<K, symbol>}`
-    : never;
-
-/**
- * A union of all values of properties in T which are not keyed by a symbol,
- * following the definition of `Object.values` and `Object.entries`.
- */
-export type EnumerableStringKeyedValueOf<T> = ValuesOf<{
-  [K in keyof T]-?: K extends symbol ? never : T[K];
-}>;
-
-/**
- * Extracts the value type from an object type T.
- */
-type ValuesOf<T> =
-  // EmptyObject is used to infer value type as never instead of unknown
-  // for an empty object
-  T extends EmptyObject
-    ? T[keyof T]
-    : T extends Record<PropertyKey, infer V>
-      ? V
-      : never;
-
-/**
- * This is the type you'd get from doing:
- * `Object.fromEntries(Object.entries(x))`.
- */
-export type ReconstructedRecord<T> = Record<
-  EnumerableStringKeyOf<T>,
-  EnumerableStringKeyedValueOf<T>
->;
-
-/**
- * An extension of Extract for type predicates which falls back to the base
- * in order to narrow the `unknown` case.
- *
- * @example
- *   function isMyType<T>(data: T | MyType): data is NarrowedTo<T, MyType> { ... }
- */
-export type NarrowedTo<T, Base> =
-  Extract<T, Base> extends never
-    ? Base
-    : IsAny<T> extends true
-      ? Base
-      : Extract<T, Base>;
-
-/**
- * A compare function that is compatible with the native `Array.sort` function.
- *
- * @returns >0 if `a` should come after `b`, 0 if they are equal, and <0 if `a` should come before `b`.
- */
-export type CompareFunction<T> = (a: T, b: T) => number;
-
-// Records with an unbounded set of keys have different semantics to those with
-// a bounded set of keys when using 'noUncheckedIndexedAccess', the former
-// being implicitly `Partial` whereas the latter are implicitly `Required`.
-export type ExactRecord<Key extends PropertyKey, Value> = IfBoundedRecord<
-  Record<Key, Value>,
-  // If the key is bounded, e.g. 'cat' | 'dog', the result is partial
-  // because we can't statically know what values the mapper would return on
-  // a specific input.
-  Partial<Record<Key, Value>>,
-  // If the key is unbounded, it means that Key is at least as wide
-  // as them, so we don't need to wrap the returned record with Partial.
-  Record<Key, Value>
->;
-
-export type ReorderedArray<T extends IterableContainer> = {
-  -readonly [P in keyof T]: T[number];
-};
-
-export type UpsertProp<T, K extends PropertyKey, V> = Simplify<
-  // Copy any uninvolved props from the object, they are unaffected by the type.
-  Omit<T, K> &
-    // Rebuild the object for the rest:
-    (IsSingleLiteral<K> extends true
-      ? // If it's a single literal we need to remove the optionality and set
-        // the value as we know this prop would be exactly this value in the
-        // output.
-        Writable<Required<Record<K, V>>>
-      : // The key is either a broad type (`string`) or union of literals
-        // ('cat' | 'dog') so we can't say anything for sure, we need to narrow
-        // the types of all relevant props, this has 2 parts, for props already
-        // in the object this means the value _might_ change, or it might not.
-        {
-          -readonly [P in keyof T as P extends K ? P : never]: T[P] | V;
-        } & {
-          // And for new props they might have been added to the object, or they
-          // might not have been, so we need to set them as optional.
-          -readonly [P in K as P extends keyof T ? never : P]?: V;
-        })
->;
-
-// This type attempts to detect when a type is a single literal value (e.g.
-// "cat"), and not anything else (e.g. "cat" | "dog", string, etc...)
-type IsSingleLiteral<K> =
-  IsLiteral<K> extends true ? (IsUnion<K> extends true ? false : true) : false;
-
-// TODO: This type is copied from type-fest because it isn't exported. It's part of the "internal" types. We should check back in a while to see if this type is added to the public offering.
-export type IsUnion<T> = InternalIsUnion<T>;
-type InternalIsUnion<T, U = T> = (
-  IsNever<T> extends true
-    ? false
-    : // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Copy pasted from type-fest
-      T extends any
-      ? [U] extends [T]
-        ? false
-        : true
-      : never
-) extends infer Result
-  ? // In some cases `Result` will return `false | true` which is `boolean`,
-    // that means `T` has at least two types and it's a union type,
-    // so we will return `true` instead of `boolean`.
-    boolean extends Result
-    ? true
-    : Result
-  : never; // Should never happen
-
-/**
- * Extracts a type predicate from a type guard function for the first argument.
- *
- * @example
- * type TypeGuardFn = (x: unknown) => x is string;
- * type Result = GuardType<TypeGuardFn>; // `string`
- */
-export type GuardType<T, Fallback = never> = T extends (
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about arguments types here
-  x: any,
-  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about arguments types here
-  ...rest: any
-) => x is infer U
-  ? U
-  : Fallback;
-
-/**
- * An array with *exactly* N elements in it.
- *
- * Only literal N values are supported. For very large N the type might result
- * in a recurse depth error. For negative N the type would result in an infinite
- * recursion. None of these have protections because this is an internal type!
- */
-export type NTuple<
-  T,
-  N extends number,
-  Result extends Array<unknown> = [],
-> = Result["length"] extends N ? Result : NTuple<T, N, [...Result, T]>;
-
-/**
- * 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"],
- *   ...Array<TupleParts<T>["item"]>,
- *   ...TupleParts<T>["suffix"],
- * ]`.
- */
-export type TupleParts<
-  T,
-  Prefix extends Array<unknown> = [],
-  Suffix extends Array<unknown> = [],
-> = T extends readonly [infer Head, ...infer Tail]
-  ? TupleParts<Tail, [...Prefix, Head], Suffix>
-  : T extends readonly [...infer Head, infer Tail]
-    ? TupleParts<Head, Prefix, [Tail, ...Suffix]>
-    : // 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
- * interchangeable.
- *
- * This type makes it easier to use the result of TupleParts when the input is a
- * fixed-length tuple but we still want to spread the rest of the array. e.g.
- * `[...CoercedArray<TupleParts<T>["item"]>, ...TupleParts<T>["suffix"]]`.
- *
- */
-export type CoercedArray<T> = IfNever<T, [], Array<T>>;
-
-/**
- * The result of running a function that would dedupe an array (`unique`,
- * `uniqueBy`, and `uniqueWith`).
- *
- * There are certain traits of the output which are unique to a deduped array
- * that allow us to create a better type; see comments inline.
- *
- * !Note: We can build better types for each of the unique functions
- * _separately_ by taking advantage of _other_ characteristics that are unique
- * to each one (e.g. in `unique` we know that each item that has a disjoint type
- * to all previous items would be part of the output, even when it isn't the
- * first), but to make this utility the most useful we kept it simple and
- * generic for now.
- */
-export type Deduped<T extends IterableContainer> = T extends readonly []
-  ? // An empty input is an empty output.
-    []
-  : T extends readonly [infer Head, ...infer Rest]
-    ? // The first item in the array is always part of the output, if our array
-      // has a first item, we can copy it over. The rest of the array is made of
-      // whatever comes after that item.
-      [Head, ...Array<Rest[number]>]
-    : T extends readonly [...Array<unknown>, unknown]
-      ? // If we don't know what the first item is, but we know that the array
-        // is non empty, we can at least say that the output is non-empty as
-        // well.
-        NonEmptyArray<T[number]>
-      : // If it's just a simple array the output is one too.
-        Array<T[number]>;
diff --git a/src/internal/types/BrandedReturn.ts b/src/internal/types/BrandedReturn.ts
new file mode 100644
index 0000000..536d39c
--- /dev/null
+++ b/src/internal/types/BrandedReturn.ts
@@ -0,0 +1,9 @@
+import type { Tagged } from "type-fest";
+
+// 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;
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any -- The most generic function signature requires the usage of `any` instead of `unknown`
+export type BrandedReturn<F extends (...args: any) => any> = (
+  ...args: Parameters<F>
+) => Tagged<ReturnType<F>, typeof TAG_NAME_BRANDED_RETURN, F>;
diff --git a/src/internal/types/CoercedArray.ts b/src/internal/types/CoercedArray.ts
new file mode 100644
index 0000000..34b28cd
--- /dev/null
+++ b/src/internal/types/CoercedArray.ts
@@ -0,0 +1,12 @@
+import type { IfNever } from "type-fest";
+
+/**
+ * `never[]` and `[]` are not the same type, and in some cases they aren't
+ * interchangeable.
+ *
+ * This type makes it easier to use the result of TupleParts when the input is a
+ * fixed-length tuple but we still want to spread the rest of the array. e.g.
+ * `[...CoercedArray<TupleParts<T>["item"]>, ...TupleParts<T>["suffix"]]`.
+ *
+ */
+export type CoercedArray<T> = IfNever<T, [], Array<T>>;
diff --git a/src/internal/types/CompareFunction.ts b/src/internal/types/CompareFunction.ts
new file mode 100644
index 0000000..6b5d2a2
--- /dev/null
+++ b/src/internal/types/CompareFunction.ts
@@ -0,0 +1,6 @@
+/**
+ * A compare function that is compatible with the native `Array.sort` function.
+ *
+ * @returns >0 if `a` should come after `b`, 0 if they are equal, and <0 if `a` should come before `b`.
+ */
+export type CompareFunction<T> = (a: T, b: T) => number;
diff --git a/src/internal/types/Deduped.ts b/src/internal/types/Deduped.ts
new file mode 100644
index 0000000..474a4f8
--- /dev/null
+++ b/src/internal/types/Deduped.ts
@@ -0,0 +1,28 @@
+import type { NonEmptyArray } from "./NonEmptyArray";
+import type { IterableContainer } from "./IterableContainer";
+
+/**
+ * The result of running a function that would dedupe an array (`unique`,
+ * `uniqueBy`, and `uniqueWith`).
+ *
+ * There are certain traits of the output which are unique to a deduped array
+ * that allow us to create a better type; see comments inline.
+ *
+ * !Note: We can build better types for each of the unique functions
+ * _separately_ by taking advantage of _other_ characteristics that are unique
+ * to each one (e.g. in `unique` we know that each item that has a disjoint type
+ * to all previous items would be part of the output, even when it isn't the
+ * first), but to make this utility the most useful we kept it simple and
+ * generic for now.
+ */
+export type Deduped<T extends IterableContainer> = T extends readonly []
+  ? []
+  : T extends readonly [infer Head, ...infer Rest]
+    ? // has a first item, we can copy it over. The rest of the array is made of
+      // whatever comes after that item.
+      [Head, ...Array<Rest[number]>]
+    : T extends readonly [...Array<unknown>, unknown]
+      ? // is non empty, we can at least say that the output is non-empty as
+        // well.
+        NonEmptyArray<T[number]>
+      : Array<T[number]>;
diff --git a/src/internal/types/EnumerableStringKeyOf.ts b/src/internal/types/EnumerableStringKeyOf.ts
new file mode 100644
index 0000000..e0d6d42
--- /dev/null
+++ b/src/internal/types/EnumerableStringKeyOf.ts
@@ -0,0 +1,13 @@
+/**
+ * A union of all keys of T which are not symbols, and where number keys are
+ * converted to strings, following the definition of `Object.keys` and
+ * `Object.entries`.
+ *
+ * Inspired and largely copied from [`sindresorhus/ts-extras`](https://github.com/sindresorhus/ts-extras/blob/44f57392c5f027268330771996c4fdf9260b22d6/source/object-keys.ts).
+ *
+ * @see EnumerableStringKeyedValueOf
+ */
+export type EnumerableStringKeyOf<T> =
+  Required<T> extends Record<infer K, unknown>
+    ? `${Exclude<K, symbol>}`
+    : never;
diff --git a/src/internal/types/EnumerableStringKeyedValueOf.ts b/src/internal/types/EnumerableStringKeyedValueOf.ts
new file mode 100644
index 0000000..5e8f469
--- /dev/null
+++ b/src/internal/types/EnumerableStringKeyedValueOf.ts
@@ -0,0 +1,21 @@
+import type { EmptyObject } from "type-fest";
+
+/**
+ * A union of all values of properties in T which are not keyed by a symbol,
+ * following the definition of `Object.values` and `Object.entries`.
+ */
+export type EnumerableStringKeyedValueOf<T> = ValuesOf<{
+  [K in keyof T]-?: K extends symbol ? never : T[K];
+}>;
+
+/**
+ * Extracts the value type from an object type T.
+ */
+type ValuesOf<T> =
+  // EmptyObject is used to infer value type as never instead of unknown
+  // for an empty object
+  T extends EmptyObject
+    ? T[keyof T]
+    : T extends Record<PropertyKey, infer V>
+      ? V
+      : never;
diff --git a/src/internal/types/ExactRecord.ts b/src/internal/types/ExactRecord.ts
new file mode 100644
index 0000000..5058949
--- /dev/null
+++ b/src/internal/types/ExactRecord.ts
@@ -0,0 +1,15 @@
+import type { IfBoundedRecord } from "./IfBoundedRecord";
+
+// Records with an unbounded set of keys have different semantics to those with
+// a bounded set of keys when using 'noUncheckedIndexedAccess', the former
+// being implicitly `Partial` whereas the latter are implicitly `Required`.
+export type ExactRecord<Key extends PropertyKey, Value> = IfBoundedRecord<
+  Record<Key, Value>,
+  // If the key is bounded, e.g. 'cat' | 'dog', the result is partial
+  // because we can't statically know what values the mapper would return on
+  // a specific input.
+  Partial<Record<Key, Value>>,
+  // If the key is unbounded, it means that Key is at least as wide
+  // as them, so we don't need to wrap the returned record with Partial.
+  Record<Key, Value>
+>;
diff --git a/src/internal/types/GuardType.ts b/src/internal/types/GuardType.ts
new file mode 100644
index 0000000..522469c
--- /dev/null
+++ b/src/internal/types/GuardType.ts
@@ -0,0 +1,15 @@
+/**
+ * Extracts a type predicate from a type guard function for the first argument.
+ *
+ * @example
+ * type TypeGuardFn = (x: unknown) => x is string;
+ * type Result = GuardType<TypeGuardFn>; // `string`
+ */
+export type GuardType<T, Fallback = never> = T extends (
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about arguments types here
+  x: any,
+  // eslint-disable-next-line @typescript-eslint/no-explicit-any -- we don't care about arguments types here
+  ...rest: any
+) => x is infer U
+  ? U
+  : Fallback;
diff --git a/src/internal/types/IfBoundedRecord.ts b/src/internal/types/IfBoundedRecord.ts
new file mode 100644
index 0000000..fb62c3f
--- /dev/null
+++ b/src/internal/types/IfBoundedRecord.ts
@@ -0,0 +1,59 @@
+import type {
+  KeysOfUnion,
+  IsStringLiteral,
+  IsNumericLiteral,
+  IsSymbolLiteral,
+  Split,
+} from "type-fest";
+
+/**
+ * Check if a type is guaranteed to be a bounded record: a record with a finite
+ * set of keys.
+ *
+ * @example
+ *     IfBoundedRecord<{ a: 1, 1: "a" }>; //=> true
+ *     IfBoundedRecord<Record<string | number, unknown>>; //=> false
+ *     IfBoundedRecord<Record<`prefix_${number}`, unknown>>; //=> false
+ */
+export type IfBoundedRecord<
+  T,
+  TypeIfBoundedRecord = true,
+  TypeIfUnboundedRecord = false,
+> =
+  IsBoundedKey<KeysOfUnion<T>> extends true
+    ? TypeIfBoundedRecord
+    : TypeIfUnboundedRecord;
+
+/**
+ * Checks if a type is a bounded key: a union of bounded strings, numeric
+ * literals, or symbol literals.
+ */
+type IsBoundedKey<T> =
+  // `extends unknown` is always going to be the case and is used to convert any
+  // union into a [distributive conditional type](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-8.html#distributive-conditional-types).
+  T extends unknown
+    ? IsStringLiteral<T> extends true
+      ? IsBoundedString<T>
+      : IsNumericLiteral<T> extends true
+        ? true
+        : IsSymbolLiteral<T>
+    : never;
+
+/**
+ * Checks if a type is a bounded string: a type that only has a finite
+ * number of strings that are that type.
+ *
+ * Most relevant for template literals: IsBoundedString<`${1 | 2}_${3 | 4}`> is
+ * true, and IsBoundedString<`${1 | 2}_${number}`> is false.
+ */
+type IsBoundedString<T> = T extends string
+  ? // (T[number] alone doesn't work because that's just string.)
+    Split<T, "">[number] extends infer U
+    ? // string. Otherwise, we assume it's bounded.
+      [`${number}`] extends [U]
+      ? false
+      : [string] extends [U]
+        ? false
+        : true
+    : false
+  : false;
diff --git a/src/internal/types/IsUnion.ts b/src/internal/types/IsUnion.ts
new file mode 100644
index 0000000..8060a07
--- /dev/null
+++ b/src/internal/types/IsUnion.ts
@@ -0,0 +1,21 @@
+import type { IsNever } from "type-fest";
+
+// TODO: This type is copied from type-fest because it isn't exported. It's part of the "internal" types. We should check back in a while to see if this type is added to the public offering.
+export type IsUnion<T> = InternalIsUnion<T>;
+
+type InternalIsUnion<T, U = T> = (
+  IsNever<T> extends true
+    ? false
+    : // eslint-disable-next-line @typescript-eslint/no-explicit-any
+      T extends any
+      ? [U] extends [T]
+        ? false
+        : true
+      : never
+) extends infer Result
+  ? // that means `T` has at least two types and it's a union type,
+    // so we will return `true` instead of `boolean`.
+    boolean extends Result
+    ? true
+    : Result
+  : never; // Should never happen
diff --git a/src/internal/types/IterableContainer.ts b/src/internal/types/IterableContainer.ts
new file mode 100644
index 0000000..113558f
--- /dev/null
+++ b/src/internal/types/IterableContainer.ts
@@ -0,0 +1,13 @@
+/**
+ * This should only be used for defining generics which extend any kind of JS
+ * array under the hood, this includes arrays *AND* tuples (of the form [x, y],
+ * and of the form [x, ...y[]], etc...), and their readonly equivalent. This
+ * allows us to be more inclusive to what functions can process.
+ *
+ * @example
+ *   function map<T extends IterableContainer>(items: T) { ... }
+ *
+ * We would've named this `ArrayLike`, but that's already used by typescript...
+ * @see This was inspired by the type-definition of Promise.all (https://github.com/microsoft/TypeScript/blob/1df5717b120cddd325deab8b0f2b2c3eecaf2b01/src/lib/es2015.promise.d.ts#L21)
+ */
+export type IterableContainer<T = unknown> = ReadonlyArray<T> | readonly [];
diff --git a/src/internal/types/Mapped.ts b/src/internal/types/Mapped.ts
new file mode 100644
index 0000000..e0f275d
--- /dev/null
+++ b/src/internal/types/Mapped.ts
@@ -0,0 +1,5 @@
+import type { IterableContainer } from "./IterableContainer";
+
+export type Mapped<T extends IterableContainer, K> = {
+  -readonly [P in keyof T]: K;
+};
diff --git a/src/internal/types/NTuple.ts b/src/internal/types/NTuple.ts
new file mode 100644
index 0000000..8692f26
--- /dev/null
+++ b/src/internal/types/NTuple.ts
@@ -0,0 +1,12 @@
+/**
+ * An array with *exactly* N elements in it.
+ *
+ * Only literal N values are supported. For very large N the type might result
+ * in a recurse depth error. For negative N the type would result in an infinite
+ * recursion. None of these have protections because this is an internal type!
+ */
+export type NTuple<
+  T,
+  N extends number,
+  Result extends Array<unknown> = [],
+> = Result["length"] extends N ? Result : NTuple<T, N, [...Result, T]>;
diff --git a/src/internal/types/NarrowedTo.ts b/src/internal/types/NarrowedTo.ts
new file mode 100644
index 0000000..e1930a6
--- /dev/null
+++ b/src/internal/types/NarrowedTo.ts
@@ -0,0 +1,15 @@
+import type { IsAny } from "type-fest";
+
+/**
+ * An extension of Extract for type predicates which falls back to the base
+ * in order to narrow the `unknown` case.
+ *
+ * @example
+ *   function isMyType<T>(data: T | MyType): data is NarrowedTo<T, MyType> { ... }
+ */
+export type NarrowedTo<T, Base> =
+  Extract<T, Base> extends never
+    ? Base
+    : IsAny<T> extends true
+      ? Base
+      : Extract<T, Base>;
diff --git a/src/internal/types/NonEmptyArray.ts b/src/internal/types/NonEmptyArray.ts
new file mode 100644
index 0000000..3c4a936
--- /dev/null
+++ b/src/internal/types/NonEmptyArray.ts
@@ -0,0 +1,1 @@
+export type NonEmptyArray<T> = [T, ...Array<T>];
diff --git a/src/internal/types/ReconstructedRecord.ts b/src/internal/types/ReconstructedRecord.ts
new file mode 100644
index 0000000..24698d5
--- /dev/null
+++ b/src/internal/types/ReconstructedRecord.ts
@@ -0,0 +1,11 @@
+import type { EnumerableStringKeyedValueOf } from "./EnumerableStringKeyedValueOf";
+import type { EnumerableStringKeyOf } from "./EnumerableStringKeyOf";
+
+/**
+ * This is the type you'd get from doing:
+ * `Object.fromEntries(Object.entries(x))`.
+ */
+export type ReconstructedRecord<T> = Record<
+  EnumerableStringKeyOf<T>,
+  EnumerableStringKeyedValueOf<T>
+>;
diff --git a/src/internal/types/RemedaTypeError.ts b/src/internal/types/RemedaTypeError.ts
new file mode 100644
index 0000000..2e4fa97
--- /dev/null
+++ b/src/internal/types/RemedaTypeError.ts
@@ -0,0 +1,22 @@
+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.`
+    >;
diff --git a/src/internal/types/ReorderedArray.ts b/src/internal/types/ReorderedArray.ts
new file mode 100644
index 0000000..add790d
--- /dev/null
+++ b/src/internal/types/ReorderedArray.ts
@@ -0,0 +1,5 @@
+import type { IterableContainer } from "./IterableContainer";
+
+export type ReorderedArray<T extends IterableContainer> = {
+  -readonly [P in keyof T]: T[number];
+};
diff --git a/src/internal/types/TupleParts.ts b/src/internal/types/TupleParts.ts
new file mode 100644
index 0000000..78174f2
--- /dev/null
+++ b/src/internal/types/TupleParts.ts
@@ -0,0 +1,45 @@
+import type { IsEqual } from "type-fest";
+
+/**
+ * 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"],
+ *   ...Array<TupleParts<T>["item"]>,
+ *   ...TupleParts<T>["suffix"],
+ * ]`.
+ */
+export type TupleParts<
+  T,
+  Prefix extends Array<unknown> = [],
+  Suffix extends Array<unknown> = [],
+> = T extends readonly [infer Head, ...infer Tail]
+  ? TupleParts<Tail, [...Prefix, Head], Suffix>
+  : T extends readonly [...infer Head, infer Tail]
+    ? TupleParts<Head, Prefix, [Tail, ...Suffix]>
+    : // 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;
diff --git a/src/internal/types/TupleSplits.ts b/src/internal/types/TupleSplits.ts
new file mode 100644
index 0000000..79fef8b
--- /dev/null
+++ b/src/internal/types/TupleSplits.ts
@@ -0,0 +1,47 @@
+import type { IterableContainer } from "./IterableContainer";
+import type { TupleParts } from "./TupleParts";
+import type { CoercedArray } from "./CoercedArray";
+
+/**
+ * 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>;
+      }
+      ?
+          | 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);
diff --git a/src/internal/types/UpsertProp.ts b/src/internal/types/UpsertProp.ts
new file mode 100644
index 0000000..a487f8d
--- /dev/null
+++ b/src/internal/types/UpsertProp.ts
@@ -0,0 +1,27 @@
+import type { IsLiteral, Simplify, Writable } from "type-fest";
+import type { IsUnion } from "./IsUnion";
+
+export type UpsertProp<T, K extends PropertyKey, V> = Simplify<
+  // Copy any uninvolved props from the object, they are unaffected by the type.
+  Omit<T, K> &
+    // Rebuild the object for the rest:
+    (IsSingleLiteral<K> extends true
+      ? // the value as we know this prop would be exactly this value in the
+        // output.
+        Writable<Required<Record<K, V>>>
+      : // ('cat' | 'dog') so we can't say anything for sure, we need to narrow
+        // the types of all relevant props, this has 2 parts, for props already
+        // in the object this means the value _might_ change, or it might not.
+        {
+          -readonly [P in keyof T as P extends K ? P : never]: T[P] | V;
+        } & {
+          // And for new props they might have been added to the object, or they
+          // might not have been, so we need to set them as optional.
+          -readonly [P in K as P extends keyof T ? never : P]?: V;
+        })
+>;
+
+// This type attempts to detect when a type is a single literal value (e.g.
+// "cat"), and not anything else (e.g. "cat" | "dog", string, etc...)
+type IsSingleLiteral<K> =
+  IsLiteral<K> extends true ? (IsUnion<K> extends true ? false : true) : false;
diff --git a/src/isArray.ts b/src/isArray.ts
index c7c0e8a..9dd7982 100644
--- a/src/isArray.ts
+++ b/src/isArray.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is an Array and narrows its type accordingly.
diff --git a/src/isBigInt.ts b/src/isBigInt.ts
index a8d51c9..5fc30c4 100644
--- a/src/isBigInt.ts
+++ b/src/isBigInt.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is a bigint and narrows its
diff --git a/src/isBoolean.ts b/src/isBoolean.ts
index 1116e04..29c45af 100644
--- a/src/isBoolean.ts
+++ b/src/isBoolean.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is a boolean and narrows its type accordingly.
diff --git a/src/isEmpty.ts b/src/isEmpty.ts
index 5038ade..39e74c8 100644
--- a/src/isEmpty.ts
+++ b/src/isEmpty.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 
 /**
  * A function that checks if the passed parameter is empty.
diff --git a/src/isIncludedIn.ts b/src/isIncludedIn.ts
index 5069678..6f4bafc 100644
--- a/src/isIncludedIn.ts
+++ b/src/isIncludedIn.ts
@@ -1,5 +1,6 @@
 import type { IsLiteral } from "type-fest";
-import type { IsUnion, IterableContainer } from "./internal/types";
+import type { IsUnion } from "./internal/types/IsUnion";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 
 /**
  * A "constant" tuple is a type that has a single runtime value that can fulfil
diff --git a/src/isNullish.ts b/src/isNullish.ts
index f5138a0..31816fa 100644
--- a/src/isNullish.ts
+++ b/src/isNullish.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is either `null` or
diff --git a/src/isNumber.ts b/src/isNumber.ts
index 9ba896b..01da0ae 100644
--- a/src/isNumber.ts
+++ b/src/isNumber.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is a number and narrows its
diff --git a/src/isObjectType.ts b/src/isObjectType.ts
index 5b7a3ca..2c80f15 100644
--- a/src/isObjectType.ts
+++ b/src/isObjectType.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * Checks if the given parameter is of type `"object"` via `typeof`, excluding `null`.
diff --git a/src/isPlainObject.ts b/src/isPlainObject.ts
index f8148a7..a654d99 100644
--- a/src/isPlainObject.ts
+++ b/src/isPlainObject.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * Checks if `data` is a "plain" object. A plain object is defined as an object with string keys and values of any type, including primitives, other objects, functions, classes, etc (aka struct/shape/record/simple). Technically, a plain object is one whose prototype is either `Object.prototype` or `null`, ensuring it does not inherit properties or methods from other object types.
diff --git a/src/isPromise.ts b/src/isPromise.ts
index f1b9da1..19c89a7 100644
--- a/src/isPromise.ts
+++ b/src/isPromise.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is a Promise and narrows its type accordingly.
diff --git a/src/isString.ts b/src/isString.ts
index 5869a2a..b4ba002 100644
--- a/src/isString.ts
+++ b/src/isString.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is a string and narrows its type accordingly.
diff --git a/src/isSymbol.ts b/src/isSymbol.ts
index 7f7961d..62ef0c6 100644
--- a/src/isSymbol.ts
+++ b/src/isSymbol.ts
@@ -1,4 +1,4 @@
-import type { NarrowedTo } from "./internal/types";
+import type { NarrowedTo } from "./internal/types/NarrowedTo";
 
 /**
  * A function that checks if the passed parameter is a symbol and narrows its type accordingly.
diff --git a/src/keys.ts b/src/keys.ts
index b2bc910..dacbfac 100644
--- a/src/keys.ts
+++ b/src/keys.ts
@@ -1,7 +1,5 @@
-import type {
-  EnumerableStringKeyOf,
-  IterableContainer,
-} from "./internal/types";
+import type { EnumerableStringKeyOf } from "./internal/types/EnumerableStringKeyOf";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Keys<T> = T extends IterableContainer ? ArrayKeys<T> : ObjectKeys<T>;
diff --git a/src/last.ts b/src/last.ts
index 1446a3a..426562e 100644
--- a/src/last.ts
+++ b/src/last.ts
@@ -1,5 +1,5 @@
 import type { LastArrayElement } from "type-fest";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Last<T extends IterableContainer> = LastArrayElement<
diff --git a/src/map.ts b/src/map.ts
index b3a8024..1364670 100644
--- a/src/map.ts
+++ b/src/map.ts
@@ -1,4 +1,5 @@
-import type { IterableContainer, Mapped } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { Mapped } from "./internal/types/Mapped";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
diff --git a/src/mapKeys.ts b/src/mapKeys.ts
index dcbfb82..361a6c5 100644
--- a/src/mapKeys.ts
+++ b/src/mapKeys.ts
@@ -3,11 +3,9 @@
  * possible!
  */
 
-import type {
-  EnumerableStringKeyOf,
-  EnumerableStringKeyedValueOf,
-  ExactRecord,
-} from "./internal/types";
+import type { EnumerableStringKeyOf } from "./internal/types/EnumerableStringKeyOf";
+import type { EnumerableStringKeyedValueOf } from "./internal/types/EnumerableStringKeyedValueOf";
+import type { ExactRecord } from "./internal/types/ExactRecord";
 import { purry } from "./purry";
 
 /**
diff --git a/src/mapValues.ts b/src/mapValues.ts
index d56b41f..855a78e 100644
--- a/src/mapValues.ts
+++ b/src/mapValues.ts
@@ -1,8 +1,6 @@
 import type { Simplify } from "type-fest";
-import type {
-  EnumerableStringKeyOf,
-  EnumerableStringKeyedValueOf,
-} from "./internal/types";
+import type { EnumerableStringKeyOf } from "./internal/types/EnumerableStringKeyOf";
+import type { EnumerableStringKeyedValueOf } from "./internal/types/EnumerableStringKeyedValueOf";
 import { purry } from "./purry";
 
 type MappedValues<T extends object, Value> = Simplify<{
diff --git a/src/mapWithFeedback.ts b/src/mapWithFeedback.ts
index fc07d95..33b58f7 100644
--- a/src/mapWithFeedback.ts
+++ b/src/mapWithFeedback.ts
@@ -1,5 +1,6 @@
 import { purryFromLazy } from "./internal/purryFromLazy";
-import type { IterableContainer, Mapped } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { Mapped } from "./internal/types/Mapped";
 import type { LazyEvaluator } from "./pipe";
 
 /**
diff --git a/src/mean.ts b/src/mean.ts
index 638fae2..2684547 100644
--- a/src/mean.ts
+++ b/src/mean.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 import { sum } from "./sum";
 
diff --git a/src/median.ts b/src/median.ts
index 17036e2..f0c41ca 100644
--- a/src/median.ts
+++ b/src/median.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Median<T extends IterableContainer<number>> =
diff --git a/src/mergeDeep.ts b/src/mergeDeep.ts
index 88d2257..dbfc51f 100644
--- a/src/mergeDeep.ts
+++ b/src/mergeDeep.ts
@@ -15,10 +15,10 @@ import { purry } from "./purry";
  * @dataFirst
  * @category Object
  */
-export function mergeDeep<
-  Destination extends Record<string, unknown>,
-  Source extends Record<string, unknown>,
->(destination: Destination, source: Source): MergeDeep<Destination, Source>;
+export function mergeDeep<Destination extends object, Source extends object>(
+  destination: Destination,
+  source: Source,
+): MergeDeep<Destination, Source>;
 
 /**
  * Merges the `source` object into the `destination` object. The merge is similar to performing `{ ...destination, ... source }` (where disjoint values from each object would be copied as-is, and for any overlapping props the value from `source` would be used); But for *each prop* (`p`), if **both** `destination` and `source` have a **plain-object** as a value, the value would be taken as the result of recursively deepMerging them (`result.p === deepMerge(destination.p, source.p)`).
@@ -35,18 +35,19 @@ export function mergeDeep<
  * @dataLast
  * @category Object
  */
-export function mergeDeep<
-  Destination extends Record<string, unknown>,
-  Source extends Record<string, unknown>,
->(source: Source): (target: Destination) => MergeDeep<Destination, Source>;
+export function mergeDeep<Source extends object>(
+  source: Source,
+): <Destination extends object>(
+  target: Destination,
+) => MergeDeep<Destination, Source>;
 
 export function mergeDeep(...args: ReadonlyArray<unknown>): unknown {
   return purry(mergeDeepImplementation, args);
 }
 
 function mergeDeepImplementation<
-  Destination extends Record<string, unknown>,
-  Source extends Record<string, unknown>,
+  Destination extends object,
+  Source extends object,
 >(destination: Destination, source: Source): MergeDeep<Destination, Source> {
   // At this point the output is already merged, simply not deeply merged.
   const output = { ...destination, ...source } as Record<
diff --git a/src/nthBy.ts b/src/nthBy.ts
index 4d15017..5a5cfe5 100644
--- a/src/nthBy.ts
+++ b/src/nthBy.ts
@@ -3,11 +3,9 @@ import {
   type OrderRule,
 } from "./internal/purryOrderRules";
 import { quickSelect } from "./internal/quickSelect";
-import type {
-  CompareFunction,
-  IterableContainer,
-  NonEmptyArray,
-} from "./internal/types";
+import type { CompareFunction } from "./internal/types/CompareFunction";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 /**
  * Retrieves the element that would be at the given index if the array were sorted according to specified rules. This function uses the *QuickSelect* algorithm running at an average complexity of *O(n)*. Semantically it is equivalent to `sortBy(data, ...rules).at(index)` which would run at *O(nlogn)*.
diff --git a/src/omitBy.ts b/src/omitBy.ts
index 7fe8d42..d9b7bf3 100644
--- a/src/omitBy.ts
+++ b/src/omitBy.ts
@@ -1,10 +1,8 @@
 import type { IfNever, Simplify } from "type-fest";
-import type {
-  EnumerableStringKeyOf,
-  EnumerableStringKeyedValueOf,
-  IfBoundedRecord,
-  ReconstructedRecord,
-} from "./internal/types";
+import type { EnumerableStringKeyOf } from "./internal/types/EnumerableStringKeyOf";
+import type { EnumerableStringKeyedValueOf } from "./internal/types/EnumerableStringKeyedValueOf";
+import type { IfBoundedRecord } from "./internal/types/IfBoundedRecord";
+import type { ReconstructedRecord } from "./internal/types/ReconstructedRecord";
 import { purry } from "./purry";
 
 // Symbols are not passed to the predicate (because they can't be enumerated
diff --git a/src/only.ts b/src/only.ts
index a29c879..01009eb 100644
--- a/src/only.ts
+++ b/src/only.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Only<T extends IterableContainer> = T extends
diff --git a/src/partialBind.ts b/src/partialBind.ts
index aa28f7a..3cb09df 100644
--- a/src/partialBind.ts
+++ b/src/partialBind.ts
@@ -2,11 +2,9 @@
  * 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";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { RemedaTypeError } from "./internal/types/RemedaTypeError";
+import type { TupleSplits } from "./internal/types/TupleSplits";
 
 type PartialBindError<Message extends string | number> = RemedaTypeError<
   "partialBind",
diff --git a/src/partialLastBind.ts b/src/partialLastBind.ts
index 285e34b..b351fe0 100644
--- a/src/partialLastBind.ts
+++ b/src/partialLastBind.ts
@@ -2,11 +2,9 @@
  * 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";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { RemedaTypeError } from "./internal/types/RemedaTypeError";
+import type { TupleSplits } from "./internal/types/TupleSplits";
 
 type PartialLastBindError<Message extends string | number> = RemedaTypeError<
   "partialLastBind",
diff --git a/src/pickBy.ts b/src/pickBy.ts
index d7b4677..a738e8c 100644
--- a/src/pickBy.ts
+++ b/src/pickBy.ts
@@ -1,10 +1,8 @@
 import type { IfNever, Simplify } from "type-fest";
-import type {
-  EnumerableStringKeyOf,
-  EnumerableStringKeyedValueOf,
-  IfBoundedRecord,
-  ReconstructedRecord,
-} from "./internal/types";
+import type { EnumerableStringKeyOf } from "./internal/types/EnumerableStringKeyOf";
+import type { EnumerableStringKeyedValueOf } from "./internal/types/EnumerableStringKeyedValueOf";
+import type { IfBoundedRecord } from "./internal/types/IfBoundedRecord";
+import type { ReconstructedRecord } from "./internal/types/ReconstructedRecord";
 import { purry } from "./purry";
 
 // Because pickBy needs to iterate over all entries of the object, only
diff --git a/src/product.ts b/src/product.ts
index 7ef5e1d..06653ed 100644
--- a/src/product.ts
+++ b/src/product.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Product<T extends IterableContainer<bigint> | IterableContainer<number>> =
diff --git a/src/pullObject.ts b/src/pullObject.ts
index 0832d77..2757afa 100644
--- a/src/pullObject.ts
+++ b/src/pullObject.ts
@@ -1,4 +1,5 @@
-import type { ExactRecord, IterableContainer } from "./internal/types";
+import type { ExactRecord } from "./internal/types/ExactRecord";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/rankBy.ts b/src/rankBy.ts
index 8a1782e..48ca50e 100644
--- a/src/rankBy.ts
+++ b/src/rankBy.ts
@@ -2,7 +2,8 @@ import {
   purryOrderRulesWithArgument,
   type OrderRule,
 } from "./internal/purryOrderRules";
-import type { CompareFunction, NonEmptyArray } from "./internal/types";
+import type { CompareFunction } from "./internal/types/CompareFunction";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 /**
  * Calculates the rank of an item in an array based on `rules`. The rank is the position where the item would appear in the sorted array. This function provides an efficient way to determine the rank in *O(n)* time, compared to *O(nlogn)* for the equivalent `sortedIndex(sortBy(data, ...rules), item)`.
diff --git a/src/sample.ts b/src/sample.ts
index 293e50f..0161e37 100644
--- a/src/sample.ts
+++ b/src/sample.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Sampled<T extends IterableContainer, N extends number> =
diff --git a/src/set.ts b/src/set.ts
index d289357..16e9f14 100644
--- a/src/set.ts
+++ b/src/set.ts
@@ -1,4 +1,4 @@
-import type { UpsertProp } from "./internal/types";
+import type { UpsertProp } from "./internal/types/UpsertProp";
 import { purry } from "./purry";
 
 /**
diff --git a/src/shuffle.ts b/src/shuffle.ts
index 84919f7..441f1e3 100644
--- a/src/shuffle.ts
+++ b/src/shuffle.ts
@@ -1,4 +1,5 @@
-import type { IterableContainer, ReorderedArray } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { ReorderedArray } from "./internal/types/ReorderedArray";
 import { purry } from "./purry";
 
 /**
diff --git a/src/sort.ts b/src/sort.ts
index ab721e8..6ba3388 100644
--- a/src/sort.ts
+++ b/src/sort.ts
@@ -1,4 +1,5 @@
-import type { IterableContainer, ReorderedArray } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { ReorderedArray } from "./internal/types/ReorderedArray";
 import { purry } from "./purry";
 
 /**
diff --git a/src/sortBy.ts b/src/sortBy.ts
index dc4cb90..fdfb126 100644
--- a/src/sortBy.ts
+++ b/src/sortBy.ts
@@ -1,10 +1,8 @@
 import { purryOrderRules, type OrderRule } from "./internal/purryOrderRules";
-import type {
-  CompareFunction,
-  IterableContainer,
-  NonEmptyArray,
-  ReorderedArray,
-} from "./internal/types";
+import type { CompareFunction } from "./internal/types/CompareFunction";
+import type { IterableContainer } from "./internal/types/IterableContainer";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
+import type { ReorderedArray } from "./internal/types/ReorderedArray";
 
 /**
  * Sorts `data` using the provided ordering rules. The `sort` is done via the
diff --git a/src/sum.ts b/src/sum.ts
index c2f5934..3a6436a 100644
--- a/src/sum.ts
+++ b/src/sum.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Sum<T extends IterableContainer<bigint> | IterableContainer<number>> =
diff --git a/src/sumBy.ts b/src/sumBy.ts
index 749fa7b..3763b36 100644
--- a/src/sumBy.ts
+++ b/src/sumBy.ts
@@ -1,5 +1,5 @@
 import { purry } from "./purry";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 
 type SumBy<
   T extends IterableContainer,
diff --git a/src/swapIndices.ts b/src/swapIndices.ts
index f8f62dd..91e266a 100644
--- a/src/swapIndices.ts
+++ b/src/swapIndices.ts
@@ -1,5 +1,5 @@
 import type { IsEqual, Join } from "type-fest";
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Difference<A extends number, B extends number> =
diff --git a/src/take.ts b/src/take.ts
index 8d19bae..db97f74 100644
--- a/src/take.ts
+++ b/src/take.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { lazyEmptyEvaluator } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
diff --git a/src/takeFirstBy.ts b/src/takeFirstBy.ts
index be7ed4d..c8bce27 100644
--- a/src/takeFirstBy.ts
+++ b/src/takeFirstBy.ts
@@ -3,7 +3,8 @@ import {
   purryOrderRulesWithArgument,
   type OrderRule,
 } from "./internal/purryOrderRules";
-import type { CompareFunction, NonEmptyArray } from "./internal/types";
+import type { CompareFunction } from "./internal/types/CompareFunction";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 /**
  * Take the first `n` items from `data` based on the provided ordering criteria. This allows you to avoid sorting the array before taking the items. The complexity of this function is *O(Nlogn)* where `N` is the length of the array.
diff --git a/src/takeLast.ts b/src/takeLast.ts
index 6a9b450..6988d2e 100644
--- a/src/takeLast.ts
+++ b/src/takeLast.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/takeLastWhile.ts b/src/takeLastWhile.ts
index 55d9ab4..9de293d 100644
--- a/src/takeLastWhile.ts
+++ b/src/takeLastWhile.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/takeWhile.ts b/src/takeWhile.ts
index b90783d..4010233 100644
--- a/src/takeWhile.ts
+++ b/src/takeWhile.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 /**
diff --git a/src/toCamelCase.ts b/src/toCamelCase.ts
index 67d400c..54b0e8b 100644
--- a/src/toCamelCase.ts
+++ b/src/toCamelCase.ts
@@ -18,7 +18,9 @@ const DEFAULT_OPTIONS = {
  * the runtime implementation of type-fest's [`CamelCase` type](https://github.com/sindresorhus/type-fest/blob/main/source/camel-case.d.ts).
  *
  * For other case manipulations see: `toLowerCase`, `toUpperCase`, `capitalize`,
- * and `uncapitalize`.
+ * `uncapitalize`, `toKebabCase`, and `toSnakeCase`.
+ *
+ * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  *
  * For *PascalCase* use `capitalize(toCamelCase(data))`.
  *
@@ -48,7 +50,7 @@ export function toCamelCase<
  * the runtime implementation of type-fest's [`CamelCase` type](https://github.com/sindresorhus/type-fest/blob/main/source/camel-case.d.ts).
  *
  * For other case manipulations see: `toLowerCase`, `toUpperCase`, `capitalize`,
- * and `uncapitalize`.
+ * `uncapitalize`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  *
diff --git a/src/toKebabCase.ts b/src/toKebabCase.ts
new file mode 100644
index 0000000..6b59157
--- /dev/null
+++ b/src/toKebabCase.ts
@@ -0,0 +1,48 @@
+import { splitWords } from "./internal/splitWords";
+import { purry } from "./purry";
+
+/**
+ * Convert a text to kebab-Case by splitting it into words and joining them back
+ * together with "-", and then lowering the case of the result.
+ *
+ * For other case manipulations see: `toLowerCase`, `toUpperCase`, `capitalize`,
+ * `uncapitalize`, and `toCamelCase`.
+ *
+ * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
+ *
+ * @param data - A string.
+ * @signature
+ *   R.toKebabCase(data);
+ * @example
+ *   R.toKebabCase("hello world"); // "hello-world"
+ *   R.toKebabCase("__HELLO_WORLD__"); // "hello-world"
+ * @dataFirst
+ * @category String
+ */
+export function toKebabCase(data: string): string;
+
+/**
+ * Convert a text to kebabCase by splitting it into words and joining them back
+ * together with "-", and then lowering the case of the result.
+ *
+ * For other case manipulations see: `toLowerCase`, `toUpperCase`, `capitalize`,
+ * `uncapitalize`, and `toCamelCase`.
+ *
+ * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
+ *
+ * @signature
+ *   R.toKebabCase()(data);
+ * @example
+ *   R.pipe("hello world", R.toKebabCase()); // "hello-world"
+ *   R.pipe("__HELLO_WORLD__", toKebabCase()); // "hello-world"
+ * @dataLast
+ * @category String
+ */
+export function toKebabCase(): (data: string) => string;
+
+export function toKebabCase(...args: ReadonlyArray<unknown>): unknown {
+  return purry(toKebabCaseImplementation, args);
+}
+
+const toKebabCaseImplementation = (data: string): string =>
+  splitWords(data).join("-").toLowerCase();
diff --git a/src/toLowerCase.ts b/src/toLowerCase.ts
index bf09cc1..b924421 100644
--- a/src/toLowerCase.ts
+++ b/src/toLowerCase.ts
@@ -6,7 +6,7 @@ import { purry } from "./purry";
  * utility type for typing.
  *
  * For other case manipulations see: `toUpperCase`, `capitalize`,
- * `uncapitalize`, and `toCamelCase`.
+ * `uncapitalize`, `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  * If the output is intended for display (on a browser) consider using
@@ -29,7 +29,7 @@ export function toLowerCase<T extends string>(data: T): Lowercase<T>;
  * utility type for typing.
  *
  * For other case manipulations see: `toUpperCase`, `capitalize`,
- * `uncapitalize`, and `toCamelCase`.
+ * `uncapitalize`, `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  * If the output is intended for display (on a browser) consider using
diff --git a/src/toSnakeCase.ts b/src/toSnakeCase.ts
new file mode 100644
index 0000000..bc90855
--- /dev/null
+++ b/src/toSnakeCase.ts
@@ -0,0 +1,48 @@
+import { splitWords } from "./internal/splitWords";
+import { purry } from "./purry";
+
+/**
+ * Convert a text to snake-case by splitting it into words and joining them back
+ * together with "_", and then lowering the case of the result.
+ *
+ * For other case manipulations see: `toLowerCase`, `toUpperCase`, `capitalize`,
+ * `uncapitalize`, `toCamelCase`, and `toKebabCase`.
+ *
+ * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
+ *
+ * @param data - A string.
+ * @signature
+ *   R.toSnakeCase(data);
+ * @example
+ *   R.toSnakeCase("hello world"); // "hello_world"
+ *   R.toSnakeCase("__HELLO_WORLD__"); // "hello_world"
+ * @dataFirst
+ * @category String
+ */
+export function toSnakeCase(data: string): string;
+
+/**
+ * Convert a text to snake-case by splitting it into words and joining them back
+ * together with "_", and then lowering the case of the result.
+ *
+ * For other case manipulations see: `toLowerCase`, `toUpperCase`, `capitalize`,
+ * `uncapitalize`, `toCamelCase`, and `toKebabCase`.
+ *
+ * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
+ *
+ * @signature
+ *   R.toSnakeCase()(data);
+ * @example
+ *   R.pipe("hello world", R.toSnakeCase()); // "hello_world"
+ *   R.pipe("__HELLO_WORLD__", toSnakeCase()); // "hello_world"
+ * @dataLast
+ * @category String
+ */
+export function toSnakeCase(): (data: string) => string;
+
+export function toSnakeCase(...args: ReadonlyArray<unknown>): unknown {
+  return purry(toSnakeCaseImplementation, args);
+}
+
+const toSnakeCaseImplementation = (data: string): string =>
+  splitWords(data).join("_").toLowerCase();
diff --git a/src/toUpperCase.ts b/src/toUpperCase.ts
index 6435f19..787f8c7 100644
--- a/src/toUpperCase.ts
+++ b/src/toUpperCase.ts
@@ -7,7 +7,7 @@ import { purry } from "./purry";
  * utility type for typing.
  *
  * For other case manipulations see: `toLowerCase`, `capitalize`,
- * `uncapitalize`, and `toCamelCase`.
+ * `uncapitalize`, `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  * If the output is intended for display (on a browser) consider using
@@ -31,7 +31,7 @@ export function toUpperCase<T extends string>(data: T): Uppercase<T>;
  * utility type for typing.
  *
  * For other case manipulations see: `toLowerCase`, `capitalize`,
- * `uncapitalize`, and `toCamelCase`.
+ * `uncapitalize`, `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  * If the output is intended for display (on a browser) consider using
diff --git a/src/uncapitalize.ts b/src/uncapitalize.ts
index 83f2f26..021fbab 100644
--- a/src/uncapitalize.ts
+++ b/src/uncapitalize.ts
@@ -7,7 +7,9 @@ import { purry } from "./purry";
  * utility type for typing.
  *
  * For other case manipulations see: `toUpperCase`, `toLowerCase`, `capitalize`,
- * and `toCamelCase`.
+ * `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
+ *
+ * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  *
  * @param data - A string.
  * @signature
@@ -26,7 +28,7 @@ export function uncapitalize<T extends string>(data: T): Uncapitalize<T>;
  * utility type for typing.
  *
  * For other case manipulations see: `toUpperCase`, `toLowerCase`, `capitalize`,
- * and `toCamelCase`.
+ * `toCamelCase`, `toKebabCase`, and `toSnakeCase`.
  *
  * !IMPORTANT: This function might work _incorrectly_ for **non-ascii** inputs.
  *
diff --git a/src/unique.ts b/src/unique.ts
index 9b46b0d..d8e2921 100644
--- a/src/unique.ts
+++ b/src/unique.ts
@@ -1,5 +1,6 @@
 import { purryFromLazy } from "./internal/purryFromLazy";
-import type { Deduped, IterableContainer } from "./internal/types";
+import type { Deduped } from "./internal/types/Deduped";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 
diff --git a/src/uniqueBy.ts b/src/uniqueBy.ts
index 0515813..ef99511 100644
--- a/src/uniqueBy.ts
+++ b/src/uniqueBy.ts
@@ -1,9 +1,7 @@
 import { purryFromLazy } from "./internal/purryFromLazy";
-import type {
-  BrandedReturn,
-  Deduped,
-  IterableContainer,
-} from "./internal/types";
+import type { BrandedReturn } from "./internal/types/BrandedReturn";
+import type { Deduped } from "./internal/types/Deduped";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 
diff --git a/src/uniqueWith.ts b/src/uniqueWith.ts
index 2024d71..065a980 100644
--- a/src/uniqueWith.ts
+++ b/src/uniqueWith.ts
@@ -1,5 +1,6 @@
 import { purryFromLazy } from "./internal/purryFromLazy";
-import type { Deduped, IterableContainer } from "./internal/types";
+import type { Deduped } from "./internal/types/Deduped";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { SKIP_ITEM } from "./internal/utilityEvaluators";
 import type { LazyEvaluator } from "./pipe";
 
diff --git a/src/values.ts b/src/values.ts
index 4405b9a..d9d2697 100644
--- a/src/values.ts
+++ b/src/values.ts
@@ -1,7 +1,5 @@
-import type {
-  EnumerableStringKeyedValueOf,
-  IterableContainer,
-} from "./internal/types";
+import type { EnumerableStringKeyedValueOf } from "./internal/types/EnumerableStringKeyedValueOf";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import { purry } from "./purry";
 
 type Values<T extends object> = T extends IterableContainer
@@ -16,7 +14,7 @@ type Values<T extends object> = T extends IterableContainer
  *    R.values(source)
  * @example
  *    R.values(['x', 'y', 'z']) // => ['x', 'y', 'z']
- *    R.values({ a: 'x', b: 'y', c: 'z' }) // => ['x', 'y', 'z']
+ *    R.values({ a: 'x', b: 'y', c: 'z' }) // => ['x', 'y', 'OOz']
  * @dataFirst
  * @category Object
  */
diff --git a/src/when.ts b/src/when.ts
index 48f3320..da292db 100644
--- a/src/when.ts
+++ b/src/when.ts
@@ -3,7 +3,7 @@
  * aren't typed as `any` in the generic type declaration.
  */
 
-import type { GuardType } from "./internal/types";
+import type { GuardType } from "./internal/types/GuardType";
 
 /**
  * Conditionally run a function based on a predicate, returning it's result (similar to
diff --git a/src/zip.ts b/src/zip.ts
index 38a9b88..87cae1a 100644
--- a/src/zip.ts
+++ b/src/zip.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import type { LazyEvaluator } from "./pipe";
 import { purry } from "./purry";
 
diff --git a/src/zipWith.ts b/src/zipWith.ts
index 9118d8e..29293d4 100644
--- a/src/zipWith.ts
+++ b/src/zipWith.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { IterableContainer } from "./internal/types/IterableContainer";
 import type { LazyEvaluator } from "./pipe";
 import { lazyDataLastImpl } from "./internal/lazyDataLastImpl";
 


Test patch:
diff --git a/src/chunk.test-d.ts b/src/chunk.test-d.ts
index cdd7c0f..b9ba74e 100644
--- a/src/chunk.test-d.ts
+++ b/src/chunk.test-d.ts
@@ -1,5 +1,5 @@
 import { chunk } from "./chunk";
-import type { NonEmptyArray } from "./internal/types";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 describe("edge-cases", () => {
   test("0 chunk size", () => {
diff --git a/src/firstBy.test-d.ts b/src/firstBy.test-d.ts
index 1c25244..0a96a71 100644
--- a/src/firstBy.test-d.ts
+++ b/src/firstBy.test-d.ts
@@ -1,6 +1,6 @@
 import { firstBy } from "./firstBy";
 import { identity } from "./identity";
-import type { NonEmptyArray } from "./internal/types";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 it("can return undefined on arrays", () => {
   const data: ReadonlyArray<number> = [1, 2, 3];
diff --git a/src/flat.test-d.ts b/src/flat.test-d.ts
index a649c1e..3d5e319 100644
--- a/src/flat.test-d.ts
+++ b/src/flat.test-d.ts
@@ -1,5 +1,5 @@
 import { flat } from "./flat";
-import type { NonEmptyArray } from "./internal/types";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 
 it("works on empty arrays", () => {
   const result = flat([], 1);
diff --git a/src/groupBy.test-d.ts b/src/groupBy.test-d.ts
index fe0c411..1ee710b 100644
--- a/src/groupBy.test-d.ts
+++ b/src/groupBy.test-d.ts
@@ -1,5 +1,5 @@
 import { groupBy } from "./groupBy";
-import type { NonEmptyArray } from "./internal/types";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 import { prop } from "./prop";
 
 test("union of string literals", () => {
diff --git a/src/internal/types.test-d.ts b/src/internal/types.test-d.ts
deleted file mode 100644
index eec5a01..0000000
--- a/src/internal/types.test-d.ts
+++ /dev/null
@@ -1,822 +0,0 @@
-import type { EmptyObject, Tagged } from "type-fest";
-import type {
-  Deduped,
-  EnumerableStringKeyedValueOf,
-  EnumerableStringKeyOf,
-  IfBoundedRecord,
-  IterableContainer,
-  NTuple,
-  TupleParts,
-  TupleSplits,
-} from "./types";
-
-declare const SymbolFoo: unique symbol;
-// eslint-disable-next-line @typescript-eslint/no-unused-vars
-declare const SymbolBar: unique symbol;
-
-describe("IfBoundedRecord", () => {
-  test("string", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<string, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Partial<Record<string, unknown>>>
-    >().toEqualTypeOf<false>();
-  });
-
-  test("number", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<number, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Partial<Record<number, unknown>>>
-    >().toEqualTypeOf<false>();
-  });
-
-  test("symbol", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<symbol, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Partial<Record<symbol, unknown>>>
-    >().toEqualTypeOf<false>();
-  });
-
-  test("union of string, number, symbol", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<number | string, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<string | symbol, unknown>>
-    >().toEqualTypeOf<false>();
-  });
-
-  test("string literals and their union", () => {
-    expectTypeOf<IfBoundedRecord<Record<"a", unknown>>>().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<"a" | "b", unknown>>
-    >().toEqualTypeOf<true>();
-  });
-
-  test("number literals and their union", () => {
-    expectTypeOf<IfBoundedRecord<Record<1, unknown>>>().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<1 | 2, unknown>>
-    >().toEqualTypeOf<true>();
-  });
-
-  test("symbol literals and their union", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<typeof SymbolFoo, unknown>>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<typeof SymbolBar | typeof SymbolFoo, unknown>>
-    >().toEqualTypeOf<true>();
-  });
-
-  test("unions between string, number, symbol", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<"a" | 1, unknown>>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<typeof SymbolFoo | "a", unknown>>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<typeof SymbolFoo | 1, unknown>>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<typeof SymbolFoo | "a" | 1, unknown>>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<{ 1: number } | { a: string; [SymbolFoo]: number }>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<{ 1?: number } | { a?: string; [SymbolFoo]: number }>
-    >().toEqualTypeOf<true>();
-  });
-
-  test("unions with unbounded types", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<number | "a", unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<string | 1, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<1, unknown> | Record<string, unknown>>
-    >().toEqualTypeOf<false>();
-  });
-
-  test("branded types", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<Tagged<string, symbol>, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<Tagged<number, symbol>, unknown>>
-    >().toEqualTypeOf<false>();
-  });
-
-  test("bounded template strings", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<`a_${1 | 2}`, unknown>>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<`${"a" | "b"}_${1 | 2}`, unknown>>
-    >().toEqualTypeOf<true>();
-
-    expectTypeOf<
-      IfBoundedRecord<
-        Record<`${1 | 2}_${1 | 2}_${1 | 2}_${1 | 2}_${1 | 2}`, unknown>
-      >
-    >().toEqualTypeOf<true>();
-  });
-
-  test("unbounded template strings", () => {
-    expectTypeOf<
-      IfBoundedRecord<Record<`a_${number}`, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<`${"a" | "b"}_${number}`, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<Record<`a_${string}`, unknown>>
-    >().toEqualTypeOf<false>();
-
-    expectTypeOf<
-      IfBoundedRecord<
-        Record<`${string}_${number}_${string}_${number}_${string}`, unknown>
-      >
-    >().toEqualTypeOf<false>();
-  });
-});
-
-describe("EnumerableStringKeyOf", () => {
-  test("string keys", () => {
-    expectTypeOf<
-      EnumerableStringKeyOf<Record<string, unknown>>
-    >().toEqualTypeOf<string>();
-  });
-
-  test("number keys", () => {
-    expectTypeOf<
-      EnumerableStringKeyOf<Record<number, unknown>>
-    >().toEqualTypeOf<`${number}`>();
-  });
-
-  test("union of records", () => {
-    expectTypeOf<
-      EnumerableStringKeyOf<
-        Record<`prefix_${string}`, unknown> | Record<number, unknown>
-      >
-    >().toEqualTypeOf<`${number}` | `prefix_${string}`>();
-  });
-
-  test("union keys", () => {
-    expectTypeOf<
-      EnumerableStringKeyOf<Record<number | `prefix_${string}`, unknown>>
-    >().toEqualTypeOf<`${number}` | `prefix_${string}`>();
-  });
-
-  test("symbol keys", () => {
-    expectTypeOf<
-      EnumerableStringKeyOf<Record<string | symbol, unknown>>
-    >().toEqualTypeOf<string>();
-
-    expectTypeOf<
-      EnumerableStringKeyOf<{ [SymbolFoo]: number; a: unknown }>
-    >().toEqualTypeOf<"a">();
-
-    expectTypeOf<
-      EnumerableStringKeyOf<Record<string | typeof SymbolFoo, unknown>>
-    >().toEqualTypeOf<string>();
-  });
-
-  test("optional keys", () => {
-    expectTypeOf<
-      EnumerableStringKeyOf<{ a: unknown; b?: unknown }>
-    >().toEqualTypeOf<"a" | "b">();
-  });
-
-  test("branded types", () => {
-    expectTypeOf<
-      EnumerableStringKeyOf<Record<Tagged<string, symbol>, unknown>>
-    >().toEqualTypeOf<`${Tagged<string, symbol>}`>();
-  });
-});
-
-describe("EnumerableStringKeyedValueOf", () => {
-  test("string values", () => {
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<Record<PropertyKey, string>>
-    >().toEqualTypeOf<string>();
-  });
-
-  test("number values", () => {
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<Record<PropertyKey, number>>
-    >().toEqualTypeOf<number>();
-  });
-
-  test("union of records", () => {
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<
-        Record<PropertyKey, "cat"> | Record<PropertyKey, "dog">
-      >
-    >().toEqualTypeOf<"cat" | "dog">();
-
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<
-        Record<PropertyKey, number> | Record<PropertyKey, string>
-      >
-    >().toEqualTypeOf<number | string>();
-  });
-
-  test("union values", () => {
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<Record<PropertyKey, number | string>>
-    >().toEqualTypeOf<number | string>();
-  });
-
-  test("literal values", () => {
-    expectTypeOf<EnumerableStringKeyedValueOf<{ a: 1 }>>().toEqualTypeOf<1>();
-
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<{ a: "1" | "2" | 1 }>
-    >().toEqualTypeOf<"1" | "2" | 1>();
-  });
-
-  test("optional values", () => {
-    expectTypeOf<EnumerableStringKeyedValueOf<{ a: 1; b?: 4 }>>().toEqualTypeOf<
-      1 | 4
-    >();
-
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<{ a: string; b?: number }>
-    >().toEqualTypeOf<number | string>();
-  });
-
-  test("nullish and undefined values", () => {
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<{
-        a: string | undefined;
-        b: string | null;
-      }>
-    >().toEqualTypeOf<string | null | undefined>();
-
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<{
-        a?: number | null;
-        b?: number | null | undefined;
-      }>
-    >().toEqualTypeOf<number | null | undefined>();
-  });
-
-  test("symbol keys", () => {
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<{ [SymbolFoo]: string }>
-    >().toEqualTypeOf<never>();
-
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<{ [SymbolFoo]: string; b: "1" }>
-    >().toEqualTypeOf<"1">();
-
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<
-        Record<PropertyKey | typeof SymbolFoo, string>
-      >
-    >().toEqualTypeOf<string>();
-  });
-
-  test("empty object", () => {
-    expectTypeOf<
-      EnumerableStringKeyedValueOf<EmptyObject>
-    >().toEqualTypeOf<never>();
-  });
-});
-
-describe("NTuple", () => {
-  test("size 0", () => {
-    const result = nTuple("foo", 0);
-    expectTypeOf(result).toEqualTypeOf<[]>();
-  });
-
-  test("non-trivial size", () => {
-    const result = nTuple("foo", 3);
-    expectTypeOf(result).toEqualTypeOf<[string, string, string]>();
-  });
-});
-
-describe("TupleParts", () => {
-  describe("mutable", () => {
-    test("empty array", () => {
-      const data = [] as [];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [];
-        item: never;
-        suffix: [];
-      }>();
-    });
-
-    test("regular array", () => {
-      const data = [] as Array<number>;
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [];
-        item: number;
-        suffix: [];
-      }>();
-    });
-
-    test("fixed tuple", () => {
-      const data = [1, 2, 3] as [1, 2, 3];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [1, 2, 3];
-        item: never;
-        suffix: [];
-      }>();
-    });
-
-    test("array with prefix", () => {
-      const data = ["a"] as [string, ...Array<boolean>];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [string];
-        item: boolean;
-        suffix: [];
-      }>();
-    });
-
-    test("array with suffix", () => {
-      const data = ["a"] as [...Array<boolean>, string];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [];
-        item: boolean;
-        suffix: [string];
-      }>();
-    });
-
-    test("array with prefix and suffix", () => {
-      const data = [1, "a"] as [number, ...Array<boolean>, string];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [number];
-        item: boolean;
-        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", () => {
-    test("empty array", () => {
-      const data = [] as const;
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [];
-        item: never;
-        suffix: [];
-      }>();
-    });
-
-    test("regular array", () => {
-      const data = [] as ReadonlyArray<number>;
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [];
-        item: number;
-        suffix: [];
-      }>();
-    });
-
-    test("fixed tuple", () => {
-      const data = [1, 2, 3] as const;
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [1, 2, 3];
-        item: never;
-        suffix: [];
-      }>();
-    });
-
-    test("array with prefix", () => {
-      const data = ["a"] as readonly [string, ...Array<boolean>];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [string];
-        item: boolean;
-        suffix: [];
-      }>();
-    });
-
-    test("array with suffix", () => {
-      const data = ["a"] as readonly [...Array<boolean>, string];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [];
-        item: boolean;
-        suffix: [string];
-      }>();
-    });
-
-    test("array with prefix and suffix", () => {
-      const data = [1, "a"] as readonly [number, ...Array<boolean>, string];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<{
-        prefix: [number];
-        item: boolean;
-        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", () => {
-    test("union of arrays", () => {
-      const data = [] as Array<boolean> | Array<number>;
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<
-        | { prefix: []; item: boolean; suffix: [] }
-        | { prefix: []; item: number; suffix: [] }
-      >();
-    });
-
-    test("mixed unions", () => {
-      const data = [] as Array<boolean> | [number, string];
-      const result = tupleParts(data);
-      expectTypeOf(result).toEqualTypeOf<
-        | { prefix: []; item: boolean; suffix: [] }
-        | { prefix: [number, string]; item: never; suffix: [] }
-      >();
-    });
-  });
-});
-
-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", () => {
-      const result = deduped([] as []);
-      expectTypeOf(result).toEqualTypeOf<[]>();
-    });
-
-    test("simple array", () => {
-      const result = deduped([1, 2, 3] as Array<number>);
-      expectTypeOf(result).toEqualTypeOf<Array<number>>();
-    });
-
-    test("array with prefix", () => {
-      const result = deduped(["a"] as [string, ...Array<number>]);
-      expectTypeOf(result).toEqualTypeOf<[string, ...Array<number>]>();
-    });
-
-    test("array with suffix", () => {
-      const result = deduped(["a"] as [...Array<number>, string]);
-      expectTypeOf(result).toEqualTypeOf<
-        [number | string, ...Array<number | string>]
-      >();
-    });
-
-    test("array with both prefix and suffix", () => {
-      const result = deduped(["a", true] as [
-        string,
-        ...Array<number>,
-        boolean,
-      ]);
-      expectTypeOf(result).toEqualTypeOf<
-        [string, ...Array<boolean | number>]
-      >();
-    });
-
-    test("union of arrays", () => {
-      const result = deduped(["a"] as
-        | [number, ...Array<number>]
-        | [string, ...Array<string>]);
-      expectTypeOf(result).toEqualTypeOf<
-        [number, ...Array<number>] | [string, ...Array<string>]
-      >();
-    });
-  });
-
-  describe("readonly", () => {
-    test("empty array", () => {
-      const result = deduped([] as const);
-      expectTypeOf(result).toEqualTypeOf<[]>();
-    });
-
-    test("simple array", () => {
-      const result = deduped([1, 2, 3] as ReadonlyArray<number>);
-      expectTypeOf(result).toEqualTypeOf<Array<number>>();
-    });
-
-    test("array with prefix", () => {
-      const result = deduped(["a"] as readonly [string, ...Array<number>]);
-      expectTypeOf(result).toEqualTypeOf<[string, ...Array<number>]>();
-    });
-
-    test("array with suffix", () => {
-      const result = deduped(["a"] as readonly [...Array<number>, string]);
-      expectTypeOf(result).toEqualTypeOf<
-        [number | string, ...Array<number | string>]
-      >();
-    });
-
-    test("array with both prefix and suffix", () => {
-      const result = deduped(["a", true] as readonly [
-        string,
-        ...Array<number>,
-        boolean,
-      ]);
-      expectTypeOf(result).toEqualTypeOf<
-        [string, ...Array<boolean | number>]
-      >();
-    });
-
-    test("union of arrays", () => {
-      const result = deduped(["a"] as
-        | readonly [number, ...Array<number>]
-        | readonly [string, ...Array<string>]);
-      expectTypeOf(result).toEqualTypeOf<
-        [number, ...Array<number>] | [string, ...Array<string>]
-      >();
-    });
-
-    test("constant tuple", () => {
-      const result = deduped([1, 2, 3] as const);
-      expectTypeOf(result).toEqualTypeOf<[1, ...Array<2 | 3>]>();
-    });
-  });
-});
-
-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/internal/types/Deduped.test-d.ts b/src/internal/types/Deduped.test-d.ts
new file mode 100644
index 0000000..1269eb9
--- /dev/null
+++ b/src/internal/types/Deduped.test-d.ts
@@ -0,0 +1,89 @@
+import type { Deduped } from "./Deduped";
+import type { IterableContainer } from "./IterableContainer";
+
+declare function deduped<T extends IterableContainer>(data: T): Deduped<T>;
+
+describe("mutable", () => {
+  test("empty array", () => {
+    const result = deduped([] as []);
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+
+  test("simple array", () => {
+    const result = deduped([1, 2, 3] as Array<number>);
+    expectTypeOf(result).toEqualTypeOf<Array<number>>();
+  });
+
+  test("array with prefix", () => {
+    const result = deduped(["a"] as [string, ...Array<number>]);
+    expectTypeOf(result).toEqualTypeOf<[string, ...Array<number>]>();
+  });
+
+  test("array with suffix", () => {
+    const result = deduped(["a"] as [...Array<number>, string]);
+    expectTypeOf(result).toEqualTypeOf<
+      [number | string, ...Array<number | string>]
+    >();
+  });
+
+  test("array with both prefix and suffix", () => {
+    const result = deduped(["a", true] as [string, ...Array<number>, boolean]);
+    expectTypeOf(result).toEqualTypeOf<[string, ...Array<boolean | number>]>();
+  });
+
+  test("union of arrays", () => {
+    const result = deduped(["a"] as
+      | [number, ...Array<number>]
+      | [string, ...Array<string>]);
+    expectTypeOf(result).toEqualTypeOf<
+      [number, ...Array<number>] | [string, ...Array<string>]
+    >();
+  });
+});
+
+describe("readonly", () => {
+  test("empty array", () => {
+    const result = deduped([] as const);
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+
+  test("simple array", () => {
+    const result = deduped([1, 2, 3] as ReadonlyArray<number>);
+    expectTypeOf(result).toEqualTypeOf<Array<number>>();
+  });
+
+  test("array with prefix", () => {
+    const result = deduped(["a"] as readonly [string, ...Array<number>]);
+    expectTypeOf(result).toEqualTypeOf<[string, ...Array<number>]>();
+  });
+
+  test("array with suffix", () => {
+    const result = deduped(["a"] as readonly [...Array<number>, string]);
+    expectTypeOf(result).toEqualTypeOf<
+      [number | string, ...Array<number | string>]
+    >();
+  });
+
+  test("array with both prefix and suffix", () => {
+    const result = deduped(["a", true] as readonly [
+      string,
+      ...Array<number>,
+      boolean,
+    ]);
+    expectTypeOf(result).toEqualTypeOf<[string, ...Array<boolean | number>]>();
+  });
+
+  test("union of arrays", () => {
+    const result = deduped(["a"] as
+      | readonly [number, ...Array<number>]
+      | readonly [string, ...Array<string>]);
+    expectTypeOf(result).toEqualTypeOf<
+      [number, ...Array<number>] | [string, ...Array<string>]
+    >();
+  });
+
+  test("constant tuple", () => {
+    const result = deduped([1, 2, 3] as const);
+    expectTypeOf(result).toEqualTypeOf<[1, ...Array<2 | 3>]>();
+  });
+});
diff --git a/src/internal/types/EnumerableStringKeyOf.test-d.ts b/src/internal/types/EnumerableStringKeyOf.test-d.ts
new file mode 100644
index 0000000..077b255
--- /dev/null
+++ b/src/internal/types/EnumerableStringKeyOf.test-d.ts
@@ -0,0 +1,56 @@
+import type { Tagged } from "type-fest";
+import type { EnumerableStringKeyOf } from "./EnumerableStringKeyOf";
+
+declare const SymbolFoo: unique symbol;
+
+test("string keys", () => {
+  expectTypeOf<
+    EnumerableStringKeyOf<Record<string, unknown>>
+  >().toEqualTypeOf<string>();
+});
+
+test("number keys", () => {
+  expectTypeOf<
+    EnumerableStringKeyOf<Record<number, unknown>>
+  >().toEqualTypeOf<`${number}`>();
+});
+
+test("union of records", () => {
+  expectTypeOf<
+    EnumerableStringKeyOf<
+      Record<`prefix_${string}`, unknown> | Record<number, unknown>
+    >
+  >().toEqualTypeOf<`${number}` | `prefix_${string}`>();
+});
+
+test("union keys", () => {
+  expectTypeOf<
+    EnumerableStringKeyOf<Record<number | `prefix_${string}`, unknown>>
+  >().toEqualTypeOf<`${number}` | `prefix_${string}`>();
+});
+
+test("symbol keys", () => {
+  expectTypeOf<
+    EnumerableStringKeyOf<Record<string | symbol, unknown>>
+  >().toEqualTypeOf<string>();
+
+  expectTypeOf<
+    EnumerableStringKeyOf<{ [SymbolFoo]: number; a: unknown }>
+  >().toEqualTypeOf<"a">();
+
+  expectTypeOf<
+    EnumerableStringKeyOf<Record<string | typeof SymbolFoo, unknown>>
+  >().toEqualTypeOf<string>();
+});
+
+test("optional keys", () => {
+  expectTypeOf<
+    EnumerableStringKeyOf<{ a: unknown; b?: unknown }>
+  >().toEqualTypeOf<"a" | "b">();
+});
+
+test("branded types", () => {
+  expectTypeOf<
+    EnumerableStringKeyOf<Record<Tagged<string, symbol>, unknown>>
+  >().toEqualTypeOf<`${Tagged<string, symbol>}`>();
+});
diff --git a/src/internal/types/EnumerableStringKeyedValueOf.test-d.ts b/src/internal/types/EnumerableStringKeyedValueOf.test-d.ts
new file mode 100644
index 0000000..0921010
--- /dev/null
+++ b/src/internal/types/EnumerableStringKeyedValueOf.test-d.ts
@@ -0,0 +1,90 @@
+import type { EmptyObject } from "type-fest";
+import type { EnumerableStringKeyedValueOf } from "./EnumerableStringKeyedValueOf";
+
+declare const SymbolFoo: unique symbol;
+
+test("string values", () => {
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<Record<PropertyKey, string>>
+  >().toEqualTypeOf<string>();
+});
+
+test("number values", () => {
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<Record<PropertyKey, number>>
+  >().toEqualTypeOf<number>();
+});
+
+test("union of records", () => {
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<
+      Record<PropertyKey, "cat"> | Record<PropertyKey, "dog">
+    >
+  >().toEqualTypeOf<"cat" | "dog">();
+
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<
+      Record<PropertyKey, number> | Record<PropertyKey, string>
+    >
+  >().toEqualTypeOf<number | string>();
+});
+
+test("union values", () => {
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<Record<PropertyKey, number | string>>
+  >().toEqualTypeOf<number | string>();
+});
+
+test("literal values", () => {
+  expectTypeOf<EnumerableStringKeyedValueOf<{ a: 1 }>>().toEqualTypeOf<1>();
+
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<{ a: "1" | "2" | 1 }>
+  >().toEqualTypeOf<"1" | "2" | 1>();
+});
+
+test("optional values", () => {
+  expectTypeOf<EnumerableStringKeyedValueOf<{ a: 1; b?: 4 }>>().toEqualTypeOf<
+    1 | 4
+  >();
+
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<{ a: string; b?: number }>
+  >().toEqualTypeOf<number | string>();
+});
+
+test("nullish and undefined values", () => {
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<{
+      a: string | undefined;
+      b: string | null;
+    }>
+  >().toEqualTypeOf<string | null | undefined>();
+
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<{
+      a?: number | null;
+      b?: number | null | undefined;
+    }>
+  >().toEqualTypeOf<number | null | undefined>();
+});
+
+test("symbol keys", () => {
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<{ [SymbolFoo]: string }>
+  >().toEqualTypeOf<never>();
+
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<{ [SymbolFoo]: string; b: "1" }>
+  >().toEqualTypeOf<"1">();
+
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<Record<PropertyKey | typeof SymbolFoo, string>>
+  >().toEqualTypeOf<string>();
+});
+
+test("empty object", () => {
+  expectTypeOf<
+    EnumerableStringKeyedValueOf<EmptyObject>
+  >().toEqualTypeOf<never>();
+});
diff --git a/src/internal/types/IfBoundedRecord.test-d.ts b/src/internal/types/IfBoundedRecord.test-d.ts
new file mode 100644
index 0000000..d813aaa
--- /dev/null
+++ b/src/internal/types/IfBoundedRecord.test-d.ts
@@ -0,0 +1,156 @@
+import type { Tagged } from "type-fest";
+import type { IfBoundedRecord } from "./IfBoundedRecord";
+
+declare const SymbolFoo: unique symbol;
+// eslint-disable-next-line @typescript-eslint/no-unused-vars
+declare const SymbolBar: unique symbol;
+
+test("string", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<string, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Partial<Record<string, unknown>>>
+  >().toEqualTypeOf<false>();
+});
+
+test("number", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<number, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Partial<Record<number, unknown>>>
+  >().toEqualTypeOf<false>();
+});
+
+test("symbol", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<symbol, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Partial<Record<symbol, unknown>>>
+  >().toEqualTypeOf<false>();
+});
+
+test("union of string, number, symbol", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<number | string, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<string | symbol, unknown>>
+  >().toEqualTypeOf<false>();
+});
+
+test("string literals and their union", () => {
+  expectTypeOf<IfBoundedRecord<Record<"a", unknown>>>().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<"a" | "b", unknown>>
+  >().toEqualTypeOf<true>();
+});
+
+test("number literals and their union", () => {
+  expectTypeOf<IfBoundedRecord<Record<1, unknown>>>().toEqualTypeOf<true>();
+
+  expectTypeOf<IfBoundedRecord<Record<1 | 2, unknown>>>().toEqualTypeOf<true>();
+});
+
+test("symbol literals and their union", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<typeof SymbolFoo, unknown>>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<typeof SymbolBar | typeof SymbolFoo, unknown>>
+  >().toEqualTypeOf<true>();
+});
+
+test("unions between string, number, symbol", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<"a" | 1, unknown>>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<typeof SymbolFoo | "a", unknown>>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<typeof SymbolFoo | 1, unknown>>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<typeof SymbolFoo | "a" | 1, unknown>>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<{ 1: number } | { a: string; [SymbolFoo]: number }>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<{ 1?: number } | { a?: string; [SymbolFoo]: number }>
+  >().toEqualTypeOf<true>();
+});
+
+test("unions with unbounded types", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<number | "a", unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<string | 1, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<1, unknown> | Record<string, unknown>>
+  >().toEqualTypeOf<false>();
+});
+
+test("branded types", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<Tagged<string, symbol>, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<Tagged<number, symbol>, unknown>>
+  >().toEqualTypeOf<false>();
+});
+
+test("bounded template strings", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<`a_${1 | 2}`, unknown>>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<`${"a" | "b"}_${1 | 2}`, unknown>>
+  >().toEqualTypeOf<true>();
+
+  expectTypeOf<
+    IfBoundedRecord<
+      Record<`${1 | 2}_${1 | 2}_${1 | 2}_${1 | 2}_${1 | 2}`, unknown>
+    >
+  >().toEqualTypeOf<true>();
+});
+
+test("unbounded template strings", () => {
+  expectTypeOf<
+    IfBoundedRecord<Record<`a_${number}`, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<`${"a" | "b"}_${number}`, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<Record<`a_${string}`, unknown>>
+  >().toEqualTypeOf<false>();
+
+  expectTypeOf<
+    IfBoundedRecord<
+      Record<`${string}_${number}_${string}_${number}_${string}`, unknown>
+    >
+  >().toEqualTypeOf<false>();
+});
diff --git a/src/internal/types/NTuple.test-d.ts b/src/internal/types/NTuple.test-d.ts
new file mode 100644
index 0000000..41429fd
--- /dev/null
+++ b/src/internal/types/NTuple.test-d.ts
@@ -0,0 +1,13 @@
+import type { NTuple } from "./NTuple";
+
+declare function nTuple<T, N extends number>(x: T, n: N): NTuple<T, N>;
+
+test("size 0", () => {
+  const result = nTuple("foo", 0);
+  expectTypeOf(result).toEqualTypeOf<[]>();
+});
+
+test("non-trivial size", () => {
+  const result = nTuple("foo", 3);
+  expectTypeOf(result).toEqualTypeOf<[string, string, string]>();
+});
diff --git a/src/internal/types/TupleParts.test-d.ts b/src/internal/types/TupleParts.test-d.ts
new file mode 100644
index 0000000..3307fc1
--- /dev/null
+++ b/src/internal/types/TupleParts.test-d.ts
@@ -0,0 +1,172 @@
+import type { TupleParts } from "./TupleParts";
+
+declare function tupleParts<T>(x: T): TupleParts<T>;
+
+describe("mutable", () => {
+  test("empty array", () => {
+    const data = [] as [];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [];
+      item: never;
+      suffix: [];
+    }>();
+  });
+
+  test("regular array", () => {
+    const data = [] as Array<number>;
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [];
+      item: number;
+      suffix: [];
+    }>();
+  });
+
+  test("fixed tuple", () => {
+    const data = [1, 2, 3] as [1, 2, 3];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [1, 2, 3];
+      item: never;
+      suffix: [];
+    }>();
+  });
+
+  test("array with prefix", () => {
+    const data = ["a"] as [string, ...Array<boolean>];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [string];
+      item: boolean;
+      suffix: [];
+    }>();
+  });
+
+  test("array with suffix", () => {
+    const data = ["a"] as [...Array<boolean>, string];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [];
+      item: boolean;
+      suffix: [string];
+    }>();
+  });
+
+  test("array with prefix and suffix", () => {
+    const data = [1, "a"] as [number, ...Array<boolean>, string];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [number];
+      item: boolean;
+      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", () => {
+  test("empty array", () => {
+    const data = [] as const;
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [];
+      item: never;
+      suffix: [];
+    }>();
+  });
+
+  test("regular array", () => {
+    const data = [] as ReadonlyArray<number>;
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [];
+      item: number;
+      suffix: [];
+    }>();
+  });
+
+  test("fixed tuple", () => {
+    const data = [1, 2, 3] as const;
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [1, 2, 3];
+      item: never;
+      suffix: [];
+    }>();
+  });
+
+  test("array with prefix", () => {
+    const data = ["a"] as readonly [string, ...Array<boolean>];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [string];
+      item: boolean;
+      suffix: [];
+    }>();
+  });
+
+  test("array with suffix", () => {
+    const data = ["a"] as readonly [...Array<boolean>, string];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [];
+      item: boolean;
+      suffix: [string];
+    }>();
+  });
+
+  test("array with prefix and suffix", () => {
+    const data = [1, "a"] as readonly [number, ...Array<boolean>, string];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<{
+      prefix: [number];
+      item: boolean;
+      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", () => {
+  test("union of arrays", () => {
+    const data = [] as Array<boolean> | Array<number>;
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<
+      | { prefix: []; item: boolean; suffix: [] }
+      | { prefix: []; item: number; suffix: [] }
+    >();
+  });
+
+  test("mixed unions", () => {
+    const data = [] as Array<boolean> | [number, string];
+    const result = tupleParts(data);
+    expectTypeOf(result).toEqualTypeOf<
+      | { prefix: []; item: boolean; suffix: [] }
+      | { prefix: [number, string]; item: never; suffix: [] }
+    >();
+  });
+});
diff --git a/src/internal/types/TupleSplits.test-d.ts b/src/internal/types/TupleSplits.test-d.ts
new file mode 100644
index 0000000..88552fc
--- /dev/null
+++ b/src/internal/types/TupleSplits.test-d.ts
@@ -0,0 +1,225 @@
+import type { IterableContainer } from "./IterableContainer";
+import type { TupleSplits } from "./TupleSplits";
+
+declare function tupleSplits<T extends IterableContainer>(x: T): TupleSplits<T>;
+
+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: [];
+        }
+    >();
+  });
+});
diff --git a/src/mergeDeep.test-d.ts b/src/mergeDeep.test-d.ts
index 524eb5a..68eeeda 100644
--- a/src/mergeDeep.test-d.ts
+++ b/src/mergeDeep.test-d.ts
@@ -1,49 +1,86 @@
 import { mergeDeep } from "./mergeDeep";
 
 it("trivially merges disjoint objects", () => {
-  const a = { foo: "bar" };
-  const b = { bar: "baz" };
-  const result = mergeDeep(a, b);
+  const result = mergeDeep(
+    { foo: "bar" } as { foo: string },
+    { bar: "baz" } as { bar: string },
+  );
   expectTypeOf(result).toEqualTypeOf<{ foo: string; bar: string }>();
 });
 
 it("merges fully overlapping types", () => {
-  const a = { foo: "bar" };
-  const b = { foo: "baz" };
-  const result = mergeDeep(a, b);
+  const result = mergeDeep(
+    { foo: "bar" } as { foo: string },
+    { foo: "baz" } as { foo: string },
+  );
   expectTypeOf(result).toEqualTypeOf<{ foo: string }>();
 });
 
 it("merges semi-overlapping types", () => {
-  const a = { foo: "bar", x: 1 };
-  const b = { foo: "baz", y: 2 };
-  const result = mergeDeep(a, b);
+  const result = mergeDeep(
+    { foo: "bar", x: 1 } as { foo: string; x: number },
+    { foo: "baz", y: 2 } as { foo: string; y: number },
+  );
   expectTypeOf(result).toEqualTypeOf<{ foo: string; x: number; y: number }>();
 });
 
 it("deeply merges", () => {
-  const a = { foo: { bar: "baz" } };
-  const b = { foo: { qux: "quux" } };
-  const result = mergeDeep(a, b);
+  const result = mergeDeep(
+    { foo: { bar: "bar" } } as { foo: { bar: string } },
+    { foo: { qux: "qux" } } as { foo: { qux: string } },
+  );
   expectTypeOf(result).toEqualTypeOf<{ foo: { bar: string; qux: string } }>();
 });
 
 it("overrides types", () => {
-  const a = { foo: { bar: "baz" } };
-  const b = { foo: "qux" };
-  expectTypeOf(mergeDeep(a, b)).toEqualTypeOf<typeof b>();
-  expectTypeOf(mergeDeep(b, a)).toEqualTypeOf<typeof a>();
+  const a = { foo: { bar: "baz" } } as { foo: { bar: string } };
+  const b = { foo: "qux" } as { foo: string };
+
+  const resultAB = mergeDeep(a, b);
+  expectTypeOf(resultAB).toEqualTypeOf<{ foo: string }>();
+
+  const resultBA = mergeDeep(b, a);
+  expectTypeOf(resultBA).toEqualTypeOf<{ foo: { bar: string } }>();
 });
 
 it("doesn't spread arrays", () => {
-  const a = { foo: ["bar"] as const };
-  const b = { foo: ["baz"] as const };
-  const result = mergeDeep(a, b);
-  expectTypeOf(result).toEqualTypeOf<{ foo: readonly ["baz"] }>();
+  const result = mergeDeep(
+    { foo: ["bar"] } as const,
+    { foo: ["baz"] } as const,
+  );
+  expectTypeOf(result).toEqualTypeOf<{ readonly foo: readonly ["baz"] }>();
 });
 
 it("doesn't recurse into arrays", () => {
-  const a = { foo: [{ bar: "baz" }] };
-  const b = { foo: [{ bar: "hello, world" }] };
-  expectTypeOf(mergeDeep(a, b)).toEqualTypeOf<typeof b>();
+  const result = mergeDeep(
+    { foo: [{ bar: "baz", x: 123 }] } as {
+      foo: Array<{ bar: string; x: number }>;
+    },
+    { foo: [{ bar: "hello, world", y: 456 }] } as {
+      foo: Array<{ bar: string; y: number }>;
+    },
+  );
+  expectTypeOf(result).toEqualTypeOf<{
+    foo: Array<{ bar: string; y: number }>;
+  }>();
+});
+
+it("works with interfaces", () => {
+  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- Intentional
+  interface Foo {
+    a: string;
+    b: number;
+  }
+
+  // eslint-disable-next-line @typescript-eslint/consistent-type-definitions -- Intentional
+  interface Bar {
+    a: string;
+    c: boolean;
+  }
+
+  const result = mergeDeep(
+    { a: "foo", b: 123 } as Foo,
+    { a: "bar", c: true } as Bar,
+  );
+  expectTypeOf(result).toEqualTypeOf<{ a: string; b: number; c: boolean }>();
 });
diff --git a/src/sample.test.ts b/src/sample.test.ts
index 30a1921..0f726eb 100644
--- a/src/sample.test.ts
+++ b/src/sample.test.ts
@@ -1,4 +1,4 @@
-import type { NonEmptyArray } from "./internal/types";
+import type { NonEmptyArray } from "./internal/types/NonEmptyArray";
 import { sample } from "./sample";
 import { times } from "./times";
 import { unique } from "./unique";
diff --git a/src/toKebabCase.test.ts b/src/toKebabCase.test.ts
new file mode 100644
index 0000000..93bf309
--- /dev/null
+++ b/src/toKebabCase.test.ts
@@ -0,0 +1,94 @@
+import { pipe } from "./pipe";
+import { toKebabCase } from "./toKebabCase";
+
+describe("data-first", () => {
+  test("on lower case", () => {
+    expect(toKebabCase("hello world")).toBe("hello-world");
+  });
+
+  test("on upper case", () => {
+    expect(toKebabCase("HELLO WORLD")).toBe("hello-world");
+  });
+
+  test("on mixed case", () => {
+    expect(toKebabCase("HeLlO WoRlD")).toBe("he-ll-o-wo-rl-d");
+  });
+
+  test("on snake case", () => {
+    expect(toKebabCase("hello_world")).toBe("hello-world");
+  });
+
+  test("on camel case", () => {
+    expect(toKebabCase("helloWorld")).toBe("hello-world");
+  });
+
+  test("on kebab case", () => {
+    expect(toKebabCase("hello-world")).toBe("hello-world");
+  });
+
+  test("on prefixed string", () => {
+    expect(toKebabCase("__HELLO_WORLD__")).toBe("hello-world");
+  });
+
+  test("on string with multiple delimiters", () => {
+    expect(toKebabCase("foo---bar")).toBe("foo-bar");
+  });
+});
+
+describe("data-last", () => {
+  test("on lower case", () => {
+    expect(pipe("hello world", toKebabCase())).toBe("hello-world");
+  });
+
+  test("on upper case", () => {
+    expect(pipe("HELLO WORLD", toKebabCase())).toBe("hello-world");
+  });
+
+  test("on mixed case", () => {
+    expect(pipe("HeLlO WoRlD", toKebabCase())).toBe("he-ll-o-wo-rl-d");
+  });
+
+  test("on snake case", () => {
+    expect(pipe("hello_world", toKebabCase())).toBe("hello-world");
+  });
+
+  test("on camel case", () => {
+    expect(pipe("helloWorld", toKebabCase())).toBe("hello-world");
+  });
+
+  test("on kebab case", () => {
+    expect(pipe("hello-world", toKebabCase())).toBe("hello-world");
+  });
+});
+
+describe("type-fest cases", () => {
+  test.each(["fooBar", "foo-bar", "foo bar", "foo_bar"])(
+    "%s to be foo-bar",
+    (str) => {
+      expect(toKebabCase(str)).toBe("foo-bar");
+    },
+  );
+
+  test("trivial case", () => {
+    expect(toKebabCase("foobar")).toBe("foobar");
+  });
+});
+
+describe.each([
+  "foo bar",
+  "Foo bar",
+  "foo Bar",
+  "Foo Bar",
+  "FOO BAR",
+  "fooBar",
+  "--foo-bar--",
+  "__foo_bar__",
+])("lodash case: %s", (str) => {
+  test("should convert string to kebab case", () => {
+    expect(toKebabCase(str)).toBe("foo-bar");
+  });
+
+  test("should handle double-converting strings", () => {
+    expect(toKebabCase(toKebabCase(str))).toBe("foo-bar");
+  });
+});
diff --git a/src/toSnakeCase.test.ts b/src/toSnakeCase.test.ts
new file mode 100644
index 0000000..0ec73a6
--- /dev/null
+++ b/src/toSnakeCase.test.ts
@@ -0,0 +1,94 @@
+import { pipe } from "./pipe";
+import { toSnakeCase } from "./toSnakeCase";
+
+describe("data-first", () => {
+  test("on lower case", () => {
+    expect(toSnakeCase("hello world")).toBe("hello_world");
+  });
+
+  test("on upper case", () => {
+    expect(toSnakeCase("HELLO WORLD")).toBe("hello_world");
+  });
+
+  test("on mixed case", () => {
+    expect(toSnakeCase("HeLlO WoRlD")).toBe("he_ll_o_wo_rl_d");
+  });
+
+  test("on snake case", () => {
+    expect(toSnakeCase("hello_world")).toBe("hello_world");
+  });
+
+  test("on camel case", () => {
+    expect(toSnakeCase("helloWorld")).toBe("hello_world");
+  });
+
+  test("on kebab case", () => {
+    expect(toSnakeCase("hello-world")).toBe("hello_world");
+  });
+
+  test("on prefixed string", () => {
+    expect(toSnakeCase("__HELLO_WORLD__")).toBe("hello_world");
+  });
+
+  test("on string with multiple delimiters", () => {
+    expect(toSnakeCase("foo---bar")).toBe("foo_bar");
+  });
+});
+
+describe("data-last", () => {
+  test("on lower case", () => {
+    expect(pipe("hello world", toSnakeCase())).toBe("hello_world");
+  });
+
+  test("on upper case", () => {
+    expect(pipe("HELLO WORLD", toSnakeCase())).toBe("hello_world");
+  });
+
+  test("on mixed case", () => {
+    expect(pipe("HeLlO WoRlD", toSnakeCase())).toBe("he_ll_o_wo_rl_d");
+  });
+
+  test("on snake case", () => {
+    expect(pipe("hello_world", toSnakeCase())).toBe("hello_world");
+  });
+
+  test("on camel case", () => {
+    expect(pipe("helloWorld", toSnakeCase())).toBe("hello_world");
+  });
+
+  test("on kebab case", () => {
+    expect(pipe("hello-world", toSnakeCase())).toBe("hello_world");
+  });
+});
+
+describe("type-fest cases", () => {
+  test.each(["fooBar", "foo-bar", "foo bar", "foo_bar"])(
+    "%s to be foo-bar",
+    (str) => {
+      expect(toSnakeCase(str)).toBe("foo_bar");
+    },
+  );
+
+  test("trivial case", () => {
+    expect(toSnakeCase("foobar")).toBe("foobar");
+  });
+});
+
+describe.each([
+  "foo bar",
+  "Foo bar",
+  "foo Bar",
+  "Foo Bar",
+  "FOO BAR",
+  "fooBar",
+  "--foo-bar--",
+  "__foo_bar__",
+])("lodash case: %s", (str) => {
+  test("should convert string to kebab case", () => {
+    expect(toSnakeCase(str)).toBe("foo_bar");
+  });
+
+  test("should handle double-converting strings", () => {
+    expect(toSnakeCase(toSnakeCase(str))).toBe("foo_bar");
+  });
+});