Repository: remeda/remeda
Pull request #686 (2024-05-30 11:48:37)

feat(hasSubObject): improve typing
> This PR depends on https://github.com/remeda/remeda/pull/683. See 35afa2fee17aa0fe16af3d467c18b22fabe23a44 for changes.

**Depends on:** https://github.com/remeda/remeda/pull/683

- make type narrowing smarter
- handle cases with overlapping union types (e.g. `{ a: string | number}, { a: number | boolean }`)
- improve typing for nested objects

---

Make sure that you:

- [x] Typedoc added for new methods and updated for changed
- [x] Tests added for new methods and updated for changed
- [x] New methods added to `src/index.ts`
- [x] New methods added to `mapping.md`

---

<details><summary>We use semantic PR titles to automate the release process!</summary>

https://conventionalcommits.org

PRs should be titled following using the format: `< TYPE >(< scope >)?: description`

### Available Types:

- `feat`: new functions, and changes to a function's type that would impact users.
- `fix`: changes to the runtime behavior of an existing function, or refinements to it's type that shouldn't impact most users.
- `perf`: changes to function implementations that improve a functions _runtime_ performance.
- `refactor`: changes to function implementations that are neither `fix` nor `perf`
- `test`: tests-only changes (transparent to users of the function).
- `docs`: changes to the documentation of a function **or the documentation site**.
- `build`, `ci`, `style`, `chore`, and `revert`: are only relevant for the internals of the library.

For scope put the name of the function you are working on (either new or
existing).

</details>


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

Latest deployment of this branch, based on commit 3f9981c35eed4eeb2fb80febd1df2e634701ca75:

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

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/remeda/remeda/pull/686?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 98.62%. Comparing base [(`96a4e82`)](https://app.codecov.io/gh/remeda/remeda/commit/96a4e8276231ba124b6ff3ac6ff997112a56d2d6?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda) to head [(`3f9981c`)](https://app.codecov.io/gh/remeda/remeda/commit/3f9981c35eed4eeb2fb80febd1df2e634701ca75?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=remeda).
> Report is 91 commits behind head on master.


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


```diff
@@            Coverage Diff             @@
##           master     #686      +/-   ##
==========================================
+ Coverage   98.41%   98.62%   +0.20%     
==========================================
  Files         127      154      +27     
  Lines        7954    11183    +3229     
  Branches      724      939     +215     
==========================================
+ Hits         7828    11029    +3201     
- Misses        123      151      +28     
  Partials        3        3              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/remeda/remeda/pull/686?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).

eranhirsch: Where does this PR stand now that #683 shipped? @lesha1201, are you planning on rebasing this?
lesha1201: @eranhirsch Yes, I will rebase it probably this week.
lesha1201: @eranhirsch Rebased.

Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 75b31bb..90d8c8d 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -7,7 +7,7 @@ Make sure that you:
 - [ ] Typedoc added for new methods and updated for changed
 - [ ] Tests added for new methods and updated for changed
 - [ ] New methods added to `src/index.ts`
-- [ ] New methods added to `docs/src/content/mapping/lodash` and `docs/src/pages/migrate/ramda.md`
+- [ ] New methods added to `docs/src/content/mapping` for both Lodash and Ramda.
 
 ---
 
diff --git a/README.md b/README.md
index 1cdeee0..19a8bb2 100644
--- a/README.md
+++ b/README.md
@@ -2,16 +2,24 @@
 
 The first "data-first" and "data-last" utility library designed especially for TypeScript.
 
-![GitHub CI](https://img.shields.io/github/actions/workflow/status/remeda/remeda/ci.yml?branch=main&label=github-ci)
-[![Codecov](https://img.shields.io/codecov/c/github/remeda/remeda/main)](https://codecov.io/gh/remeda/remeda)
-[![NPM](https://img.shields.io/npm/v/remeda)](https://www.npmjs.org/package/remeda)
-![Dependencies](https://img.shields.io/librariesio/release/npm/remeda)
+[![GitHub License](https://img.shields.io/github/license/remeda/remeda?style=flat-square)](https://github.com/remeda/remeda?tab=MIT-1-ov-file#readme)
+![GitHub top language](https://img.shields.io/github/languages/top/remeda/remeda?logo=typescript&style=flat-square)
+[![NPM](https://img.shields.io/npm/v/remeda?logo=npm&style=flat-square)](https://www.npmjs.org/package/remeda)
+[![NPM Downloads](https://img.shields.io/npm/dm/remeda?logo=npm&style=flat-square)](https://npmtrends.com/remeda)
+![GitHub Repo stars](https://img.shields.io/github/stars/remeda/remeda?logo=github&style=flat-square)
+[![Libraries.io SourceRank](https://img.shields.io/librariesio/sourcerank/npm/remeda?logo=librariesdotio&style=flat-square)](https://libraries.io/npm/remeda/sourcerank)<br />
+[![Codecov](https://img.shields.io/codecov/c/github/remeda/remeda?logo=codecov&style=flat-square)](https://codecov.io/gh/remeda/remeda)
+[![GitHub branch status](https://img.shields.io/github/checks-status/remeda/remeda/main?logo=github&style=flat-square)](https://github.com/remeda/remeda/actions/workflows)
+[![GitHub Release Date](https://img.shields.io/github/release-date/remeda/remeda?logo=npm&style=flat-square)](https://github.com/remeda/remeda/releases)
+[![Commits](https://img.shields.io/github/commit-activity/y/remeda/remeda?logo=github&style=flat-square)](https://github.com/remeda/remeda/commits/main/)
+[![All contributors](https://img.shields.io/github/contributors/remeda/remeda?logo=github&style=flat-square)](https://github.com/remeda/remeda/graphs/contributors)
+
 
 ## Documentation
 
 Read the full docs and API reference on [remedajs.com/docs](https://remedajs.com/docs).
 
-Migrating from Lodash or Ramda? Check the function mapping on [remedajs.com/mapping](https://remedajs.com/mapping).
+Migrating from other libraries? Check out our migration guides for [Lodash](https://remedajs.com/migrate/lodash) and [Ramda](https://remedajs.com/migrate/ramda)!
 
 ## Features
 
diff --git a/docs/src/components/docs-page-nav.astro b/docs/src/components/docs-page-nav.astro
index 685f2d9..94f4d28 100644
--- a/docs/src/components/docs-page-nav.astro
+++ b/docs/src/components/docs-page-nav.astro
@@ -31,6 +31,7 @@ const navbarEntries =
 
 <MobileNav
   showVersionSelector={migrationLibrary === undefined}
+  title={migrationLibrary}
   pathname={pathname}
   entries={navbarEntries}
   client:media="(max-width: 767px)"
diff --git a/docs/src/components/header.astro b/docs/src/components/header.astro
index 08cab08..1ddd9cb 100644
--- a/docs/src/components/header.astro
+++ b/docs/src/components/header.astro
@@ -4,6 +4,7 @@ import { GitHubLogoIcon } from "@radix-ui/react-icons";
 import DocsPageNav from "./docs-page-nav.astro";
 import NavLink from "./nav-link.astro";
 import { ThemeSwitcher } from "./theme-switcher";
+import MigrationSelector from "./migration-selector.astro";
 ---
 
 <header
@@ -14,7 +15,10 @@ import { ThemeSwitcher } from "./theme-switcher";
   >
     <a href="/" class="font-bold">Remeda</a>
     <NavLink href="/docs" additionalHrefs={["/v1"]}>Documentation</NavLink>
-    <NavLink directory href="/migrate">Migration Guides</NavLink>
+    <MigrationSelector
+      ><NavLink directory as="span" href="/migrate">Migration Guides</NavLink
+      ></MigrationSelector
+    >
     <div class="flex-1"></div>
     <a
       href="https://github.com/remeda/remeda"
diff --git a/docs/src/components/mapping-function-card.astro b/docs/src/components/mapping-function-card.astro
index 7b22063..d5005a5 100644
--- a/docs/src/components/mapping-function-card.astro
+++ b/docs/src/components/mapping-function-card.astro
@@ -4,7 +4,7 @@ import { getEntry } from "astro:content";
 import { Badge } from "./ui/badge";
 import Prose from "./prose.astro";
 import { ArrowRightIcon } from "@radix-ui/react-icons";
-import { mappingUrl } from "@/lib/mappings";
+import { mappingUrl, type Library } from "@/lib/mappings";
 import GithubEdit from "./github-edit.astro";
 import invariant from "tiny-invariant";
 
@@ -12,7 +12,7 @@ const COLLECTION = "mapping";
 
 type Props = {
   readonly name: string;
-  readonly library: string;
+  readonly library: Library;
 };
 
 const { name, library } = Astro.props;
diff --git a/docs/src/components/migration-selector-island.tsx b/docs/src/components/migration-selector-island.tsx
new file mode 100644
index 0000000..297ab2d
--- /dev/null
+++ b/docs/src/components/migration-selector-island.tsx
@@ -0,0 +1,31 @@
+import {
+  DropdownMenu,
+  DropdownMenuContent,
+  DropdownMenuItem,
+  DropdownMenuTrigger,
+} from "@/components/ui/dropdown-menu";
+import type { Library } from "@/lib/mappings";
+import type { ReactNode } from "react";
+
+export function MigrationSelectorIsland({
+  libraries,
+  children,
+}: {
+  readonly libraries: ReadonlyArray<Library>;
+  readonly children: ReactNode;
+}) {
+  return (
+    <DropdownMenu modal={false}>
+      <DropdownMenuTrigger asChild>
+        <button type="button">{children}</button>
+      </DropdownMenuTrigger>
+      <DropdownMenuContent>
+        {libraries.map((library) => (
+          <DropdownMenuItem key={library} className="capitalize">
+            <a href={`/migrate/${library}`}>{library}</a>
+          </DropdownMenuItem>
+        ))}
+      </DropdownMenuContent>
+    </DropdownMenu>
+  );
+}
diff --git a/docs/src/components/migration-selector.astro b/docs/src/components/migration-selector.astro
new file mode 100644
index 0000000..49ff00d
--- /dev/null
+++ b/docs/src/components/migration-selector.astro
@@ -0,0 +1,10 @@
+---
+import { getLibraries } from "@/lib/mappings";
+import { MigrationSelectorIsland } from "./migration-selector-island";
+
+const libraries = await getLibraries();
+---
+
+<MigrationSelectorIsland client:load libraries={libraries}
+  ><slot /></MigrationSelectorIsland
+>
diff --git a/docs/src/components/mobile-nav.tsx b/docs/src/components/mobile-nav.tsx
index d748197..b542457 100644
--- a/docs/src/components/mobile-nav.tsx
+++ b/docs/src/components/mobile-nav.tsx
@@ -9,10 +9,12 @@ import { isEmpty } from "remeda";
 export function MobileNav({
   pathname,
   entries,
+  title,
   showVersionSelector = false,
 }: {
   readonly pathname: string;
   readonly entries: ReadonlyArray<NavbarCategory>;
+  readonly title: string | undefined;
   readonly showVersionSelector?: boolean;
 }): ReactNode {
   const [isOpen, setIsOpen] = useState(false);
@@ -35,6 +37,9 @@ export function MobileNav({
             setIsOpen(false);
           }}
         >
+          {title !== undefined && (
+            <h2 className="text-lg font-bold capitalize">{title}</h2>
+          )}
           {showVersionSelector && <VersionSelector pathname={pathname} />}
         </Navbar>
       </SheetContent>
diff --git a/docs/src/components/nav-link.astro b/docs/src/components/nav-link.astro
index c4e92b7..40a1e76 100644
--- a/docs/src/components/nav-link.astro
+++ b/docs/src/components/nav-link.astro
@@ -3,12 +3,14 @@ type Props = {
   readonly href: string;
   readonly additionalHrefs?: readonly string[];
   readonly directory?: boolean;
+  readonly ["as"]?: "span" | "a";
 };
 
 const {
   href,
   additionalHrefs = [],
   directory: isDirectory = false,
+  as: asHtmlElement = "a",
 } = Astro.props;
 
 const { pathname } = Astro.url;
@@ -17,14 +19,21 @@ const trimmed = pathname.replace(/\/$/, "");
 const isActive = [href, ...additionalHrefs].some((path) =>
   isDirectory ? trimmed.startsWith(path) : path === trimmed,
 );
+
+const classList = [
+  "truncate transition-colors hover:text-foreground/80",
+  isActive ? "font-semibold text-primary" : "text-foreground/60",
+] as const;
 ---
 
-<a
-  href={href}
-  class:list={[
-    "truncate transition-colors hover:text-foreground/80",
-    isActive ? "font-semibold text-primary" : "text-foreground/60",
-  ]}
->
-  <slot />
-</a>
+{
+  asHtmlElement === "a" ? (
+    <a href={href} class:list={classList}>
+      <slot />
+    </a>
+  ) : (
+    <span class:list={classList}>
+      <slot />
+    </span>
+  )
+}
diff --git a/docs/src/content/mapping/lodash/__MISSING.md b/docs/src/content/mapping/lodash/__MISSING.md
index ea20efc..aeb2dc7 100644
--- a/docs/src/content/mapping/lodash/__MISSING.md
+++ b/docs/src/content/mapping/lodash/__MISSING.md
@@ -144,7 +144,6 @@ TODO: Go over: https://you-dont-need.github.io/You-Dont-Need-Lodash-Underscore/#
 - repeat
 - replace
 - snakeCase
-- split
 - startCase
 - startsWith
 - template
diff --git a/docs/src/content/mapping/lodash/includes.md b/docs/src/content/mapping/lodash/includes.md
index d98d3e3..c0b77ba 100644
--- a/docs/src/content/mapping/lodash/includes.md
+++ b/docs/src/content/mapping/lodash/includes.md
@@ -35,7 +35,6 @@ isIncludedIn(1, values({ a: 1, b: 2 }));
 ### Strings
 
 ```ts
-// Lodash
 const DATA = "Hello, World!";
 
 // Lodash
diff --git a/docs/src/content/mapping/lodash/split.md b/docs/src/content/mapping/lodash/split.md
new file mode 100644
index 0000000..d37386c
--- /dev/null
+++ b/docs/src/content/mapping/lodash/split.md
@@ -0,0 +1,4 @@
+---
+category: String
+remeda: split
+---
diff --git a/docs/src/content/mapping/ramda/F.md b/docs/src/content/mapping/ramda/F.md
new file mode 100644
index 0000000..dd8e114
--- /dev/null
+++ b/docs/src/content/mapping/ramda/F.md
@@ -0,0 +1,16 @@
+---
+category: Function
+remeda: constant
+---
+
+Use `constant` with the constant `false`. Notice that in Ramda you use `F`
+itself as the function, but in Remeda `constant` is a factory that _creates_ the
+replacement function.
+
+```ts
+// Ramda
+F;
+
+// Remeda
+constant(false);
+```
diff --git a/docs/src/content/mapping/ramda/T.md b/docs/src/content/mapping/ramda/T.md
new file mode 100644
index 0000000..64a3685
--- /dev/null
+++ b/docs/src/content/mapping/ramda/T.md
@@ -0,0 +1,16 @@
+---
+category: Function
+remeda: constant
+---
+
+Use `constant` with the constant `true`. Notice that in Ramda you use `T` itself
+as the function, but in Remeda `constant` is a factory that _creates_ the
+replacement function.
+
+```ts
+// Ramda
+T;
+
+// Remeda
+constant(true);
+```
diff --git a/docs/src/content/mapping/ramda/__MISSING.md b/docs/src/content/mapping/ramda/__MISSING.md
new file mode 100644
index 0000000..1d3a898
--- /dev/null
+++ b/docs/src/content/mapping/ramda/__MISSING.md
@@ -0,0 +1,183 @@
+# Function
+
+- addIndexRight
+- andThen
+- ap
+- apply
+- applySpec
+- applyTo
+- ascend
+- ascendNatural
+- binary
+- bind
+- call
+- comparator
+- compose
+- composeWith
+- construct
+- constructN
+- converge
+- curry
+- curryN
+- descend
+- descendNatural
+- empty
+- flow
+- invoker
+- juxt
+- lift
+- liftN
+- memoizeWith
+- nAry
+- nthArg
+- o
+- of
+- on
+- otherwise
+- partial
+- partialObject
+- partialRight
+- pipeWith
+- promap
+- thunkify
+- tryCatch
+- unapply
+- unary
+- uncurryN
+- useWith
+
+# List
+
+- adjust
+- all
+- any
+- aperture
+- collectBy
+- count
+- dropRepeats
+- dropRepeatsBy
+- dropRepeatsWith
+- endsWith
+- groupWith
+- indexOf
+- insert
+- insertAll
+- intersperse
+- into
+- lastIndexOf
+- mapAccumRight
+- move
+- none
+- pair
+- pluck
+- prepend
+- reduceBy
+- reduced
+- reduceRight
+- reduceWhile
+- remove
+- repeat
+- scan
+- sequence
+- splitEvery
+- splitWhenever
+- startsWith
+- transduce
+- transpose
+- traverse
+- unfold
+- unnest
+- update
+- without
+- xprod
+
+# Logic
+
+- and
+- both
+- complement
+- defaultTo
+- either
+- ifElse
+- isNotEmpty
+- or
+- pathSatisfies
+- propSatisfies
+- unless
+- until
+- when
+- xor
+
+# Math
+
+- mathMod
+- median
+- modulo
+
+# Object
+
+- dissoc
+- dissocPath
+- eqProps
+- has
+- hasIn
+- hasPath
+- invert
+- keysIn
+- lens
+- lensIndex
+- lensPath
+- lensProp
+- mergeDeepLeft
+- mergeDeepWith
+- mergeDeepWithKey
+- mergeLeft
+- mergeWith
+- mergeWithKey
+- modify
+- modifyPath
+- over
+- path
+- paths
+- pickAll
+- project
+- propOr
+- props
+- toPairsIn
+- unwind
+- valuesIn
+- view
+- whereAny
+- whereEq
+
+# Relation
+
+- countBy
+- eqBy
+- gt
+- gte
+- identical
+- lt
+- lte
+- pathEq
+- propEq
+- symmetricDifference
+- symmetricDifferenceWith
+- union
+- unionWith
+
+# String
+
+- match
+- replace
+- test
+- toLower
+- toString
+- toUpper
+- trim
+
+# Type
+
+- type
+- is
+- propIs
diff --git a/docs/src/content/mapping/ramda/___TODO.md b/docs/src/content/mapping/ramda/___TODO.md
new file mode 100644
index 0000000..c54a614
--- /dev/null
+++ b/docs/src/content/mapping/ramda/___TODO.md
@@ -0,0 +1,7 @@
+1. Document the difference between Ramda's difference and intersection and ours
+   in regards to duplicate values.
+
+2. Check how Ramda handles symbols in functions like omit, pick...
+
+3. Improve the docs for `set`: we need to mention the difference between addProp
+   and set, and to document the difference in syntax between Ramda and us.
diff --git a/docs/src/content/mapping/ramda/add.md b/docs/src/content/mapping/ramda/add.md
new file mode 100644
index 0000000..921e36d
--- /dev/null
+++ b/docs/src/content/mapping/ramda/add.md
@@ -0,0 +1,4 @@
+---
+category: Math
+remeda: add
+---
diff --git a/docs/src/content/mapping/ramda/addIndex.md b/docs/src/content/mapping/ramda/addIndex.md
new file mode 100644
index 0000000..3c39489
--- /dev/null
+++ b/docs/src/content/mapping/ramda/addIndex.md
@@ -0,0 +1,18 @@
+---
+category: Function
+---
+
+_Not provided by Remeda._
+
+You don't need `addIndex` In Remeda because all functions that iterate over
+arrays or objects have their callbacks in the format that `addIndex` returns by
+default. If you still have a use-case that isn't possible via Remeda please open
+an issue at the Remeda GitHub project.
+
+```ts
+// Ramda
+addIndex(map)((val, idx) => idx + "-" + val, DATA);
+
+// Remeda
+map(DATA, (val, idx) => idx + "-" + val);
+```
diff --git a/docs/src/content/mapping/ramda/allPass.md b/docs/src/content/mapping/ramda/allPass.md
new file mode 100644
index 0000000..31f7184
--- /dev/null
+++ b/docs/src/content/mapping/ramda/allPass.md
@@ -0,0 +1,4 @@
+---
+category: Logic
+remeda: allPass
+---
diff --git a/docs/src/content/mapping/ramda/always.md b/docs/src/content/mapping/ramda/always.md
new file mode 100644
index 0000000..e522035
--- /dev/null
+++ b/docs/src/content/mapping/ramda/always.md
@@ -0,0 +1,4 @@
+---
+category: Function
+remeda: constant
+---
diff --git a/docs/src/content/mapping/ramda/anyPass.md b/docs/src/content/mapping/ramda/anyPass.md
new file mode 100644
index 0000000..755f534
--- /dev/null
+++ b/docs/src/content/mapping/ramda/anyPass.md
@@ -0,0 +1,4 @@
+---
+category: Logic
+remeda: anyPass
+---
diff --git a/docs/src/content/mapping/ramda/append.md b/docs/src/content/mapping/ramda/append.md
new file mode 100644
index 0000000..3b8354a
--- /dev/null
+++ b/docs/src/content/mapping/ramda/append.md
@@ -0,0 +1,35 @@
+---
+category: List
+remeda: concat
+---
+
+_Not provided by Remeda._
+
+- You can replicate this function via the native JS spread `...` operator or via
+  the Remeda `concat` function (by wrapping the operand with an array).
+- There might be cases where building an `append` function could be more
+  efficient than using the suggested alternatives, but we believe they are
+  extremely rare. If you want to suggest adding it reach out to us at the Remeda
+  GitHub project.
+
+```ts
+// Ramda
+append(operand, DATA);
+
+// Or curried
+const addsAbc = append(operand);
+
+// Remeda
+concat(DATA, [operand]);
+
+// Or in a pipe
+pipe(DATA, concat([operand]));
+
+// Native
+[...DATA, operand];
+
+const addsAbc = <T extends ReadonlyArray<unknown>>(data: T) => [
+  ...data,
+  operand,
+];
+```
diff --git a/docs/src/content/mapping/ramda/assoc.md b/docs/src/content/mapping/ramda/assoc.md
new file mode 100644
index 0000000..68d6c99
--- /dev/null
+++ b/docs/src/content/mapping/ramda/assoc.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: set
+---
diff --git a/docs/src/content/mapping/ramda/assocPath.md b/docs/src/content/mapping/ramda/assocPath.md
new file mode 100644
index 0000000..c3167fd
--- /dev/null
+++ b/docs/src/content/mapping/ramda/assocPath.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: setPath
+---
diff --git a/docs/src/content/mapping/ramda/chain.md b/docs/src/content/mapping/ramda/chain.md
new file mode 100644
index 0000000..5bbcbb9
--- /dev/null
+++ b/docs/src/content/mapping/ramda/chain.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: flatMap
+---
diff --git a/docs/src/content/mapping/ramda/clamp.md b/docs/src/content/mapping/ramda/clamp.md
new file mode 100644
index 0000000..6db9ca0
--- /dev/null
+++ b/docs/src/content/mapping/ramda/clamp.md
@@ -0,0 +1,4 @@
+---
+category: Relation
+remeda: clamp
+---
diff --git a/docs/src/content/mapping/ramda/clone.md b/docs/src/content/mapping/ramda/clone.md
new file mode 100644
index 0000000..a33ac86
--- /dev/null
+++ b/docs/src/content/mapping/ramda/clone.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: clone
+---
diff --git a/docs/src/content/mapping/ramda/concat.md b/docs/src/content/mapping/ramda/concat.md
new file mode 100644
index 0000000..a30a791
--- /dev/null
+++ b/docs/src/content/mapping/ramda/concat.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: concat
+---
diff --git a/docs/src/content/mapping/ramda/cond.md b/docs/src/content/mapping/ramda/cond.md
new file mode 100644
index 0000000..bf2b685
--- /dev/null
+++ b/docs/src/content/mapping/ramda/cond.md
@@ -0,0 +1,4 @@
+---
+category: Logic
+remeda: conditional
+---
diff --git a/docs/src/content/mapping/ramda/dec.md b/docs/src/content/mapping/ramda/dec.md
new file mode 100644
index 0000000..6d4411a
--- /dev/null
+++ b/docs/src/content/mapping/ramda/dec.md
@@ -0,0 +1,14 @@
+---
+category: Math
+remeda: subtract
+---
+
+Use `subtract` with an operand of `1`.
+
+```ts
+// Ramda
+dec(value);
+
+// Remeda
+subtract(value, 1);
+```
diff --git a/docs/src/content/mapping/ramda/difference.md b/docs/src/content/mapping/ramda/difference.md
new file mode 100644
index 0000000..dc6be81
--- /dev/null
+++ b/docs/src/content/mapping/ramda/difference.md
@@ -0,0 +1,4 @@
+---
+category: Relation
+remeda: difference
+---
diff --git a/docs/src/content/mapping/ramda/differenceWith.md b/docs/src/content/mapping/ramda/differenceWith.md
new file mode 100644
index 0000000..7f6fa45
--- /dev/null
+++ b/docs/src/content/mapping/ramda/differenceWith.md
@@ -0,0 +1,4 @@
+---
+category: Relation
+remeda: differenceWith
+---
diff --git a/docs/src/content/mapping/ramda/divide.md b/docs/src/content/mapping/ramda/divide.md
new file mode 100644
index 0000000..eb87f1d
--- /dev/null
+++ b/docs/src/content/mapping/ramda/divide.md
@@ -0,0 +1,4 @@
+---
+category: Math
+remeda: divide
+---
diff --git a/docs/src/content/mapping/ramda/drop.md b/docs/src/content/mapping/ramda/drop.md
new file mode 100644
index 0000000..3c1d466
--- /dev/null
+++ b/docs/src/content/mapping/ramda/drop.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: drop
+---
diff --git a/docs/src/content/mapping/ramda/dropLast.md b/docs/src/content/mapping/ramda/dropLast.md
new file mode 100644
index 0000000..1001e21
--- /dev/null
+++ b/docs/src/content/mapping/ramda/dropLast.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: dropLast
+---
diff --git a/docs/src/content/mapping/ramda/dropLastWhile.md b/docs/src/content/mapping/ramda/dropLastWhile.md
new file mode 100644
index 0000000..e9b1fbc
--- /dev/null
+++ b/docs/src/content/mapping/ramda/dropLastWhile.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: dropLastWhile
+---
diff --git a/docs/src/content/mapping/ramda/dropWhile.md b/docs/src/content/mapping/ramda/dropWhile.md
new file mode 100644
index 0000000..efcbc9d
--- /dev/null
+++ b/docs/src/content/mapping/ramda/dropWhile.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: dropWhile
+---
diff --git a/docs/src/content/mapping/ramda/equals.md b/docs/src/content/mapping/ramda/equals.md
new file mode 100644
index 0000000..408f973
--- /dev/null
+++ b/docs/src/content/mapping/ramda/equals.md
@@ -0,0 +1,4 @@
+---
+category: Relation
+remeda: isDeepEqual
+---
diff --git a/docs/src/content/mapping/ramda/evolve.md b/docs/src/content/mapping/ramda/evolve.md
new file mode 100644
index 0000000..8032492
--- /dev/null
+++ b/docs/src/content/mapping/ramda/evolve.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: evolve
+---
diff --git a/docs/src/content/mapping/ramda/filter.md b/docs/src/content/mapping/ramda/filter.md
new file mode 100644
index 0000000..9d0bf60
--- /dev/null
+++ b/docs/src/content/mapping/ramda/filter.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: filter
+---
diff --git a/docs/src/content/mapping/ramda/find.md b/docs/src/content/mapping/ramda/find.md
new file mode 100644
index 0000000..001dbfe
--- /dev/null
+++ b/docs/src/content/mapping/ramda/find.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: find
+---
diff --git a/docs/src/content/mapping/ramda/findIndex.md b/docs/src/content/mapping/ramda/findIndex.md
new file mode 100644
index 0000000..3b5dda1
--- /dev/null
+++ b/docs/src/content/mapping/ramda/findIndex.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: findIndex
+---
diff --git a/docs/src/content/mapping/ramda/findLast.md b/docs/src/content/mapping/ramda/findLast.md
new file mode 100644
index 0000000..0bb9879
--- /dev/null
+++ b/docs/src/content/mapping/ramda/findLast.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: findLast
+---
diff --git a/docs/src/content/mapping/ramda/findLastIndex.md b/docs/src/content/mapping/ramda/findLastIndex.md
new file mode 100644
index 0000000..23caec1
--- /dev/null
+++ b/docs/src/content/mapping/ramda/findLastIndex.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: findLastIndex
+---
diff --git a/docs/src/content/mapping/ramda/flatten.md b/docs/src/content/mapping/ramda/flatten.md
new file mode 100644
index 0000000..a556072
--- /dev/null
+++ b/docs/src/content/mapping/ramda/flatten.md
@@ -0,0 +1,18 @@
+---
+category: List
+remeda: flat
+---
+
+Unlike `flatten` in Ramda, the Remeda `flat` function is always bound by the
+`depth` param. To replicate the Ramda behavior use a high, **const** value.
+`Infinity` and `Number.MAX_INTEGER` are not consts and would result in
+inefficient typing.
+
+```ts
+// Ramda
+flatten(DATA);
+
+// Remeda
+flat(DATA, 100); // ✅
+flat(DATA, Infinity); // ❌
+```
diff --git a/docs/src/content/mapping/ramda/forEach.md b/docs/src/content/mapping/ramda/forEach.md
new file mode 100644
index 0000000..e9f2d99
--- /dev/null
+++ b/docs/src/content/mapping/ramda/forEach.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: forEach
+---
diff --git a/docs/src/content/mapping/ramda/forEachObjIndexed.md b/docs/src/content/mapping/ramda/forEachObjIndexed.md
new file mode 100644
index 0000000..62fb58e
--- /dev/null
+++ b/docs/src/content/mapping/ramda/forEachObjIndexed.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: forEachObj
+---
diff --git a/docs/src/content/mapping/ramda/fromPairs.md b/docs/src/content/mapping/ramda/fromPairs.md
new file mode 100644
index 0000000..35c6411
--- /dev/null
+++ b/docs/src/content/mapping/ramda/fromPairs.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: fromEntries
+---
diff --git a/docs/src/content/mapping/ramda/groupBy.md b/docs/src/content/mapping/ramda/groupBy.md
new file mode 100644
index 0000000..d57d2c3
--- /dev/null
+++ b/docs/src/content/mapping/ramda/groupBy.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: groupBy
+---
diff --git a/docs/src/content/mapping/ramda/head.md b/docs/src/content/mapping/ramda/head.md
new file mode 100644
index 0000000..1cc5fe6
--- /dev/null
+++ b/docs/src/content/mapping/ramda/head.md
@@ -0,0 +1,26 @@
+---
+category: List
+remeda: first
+---
+
+- For strings use [`sliceString`](/docs#sliceString) with `0` and `1` as arguments instead.
+
+### Arrays
+
+```ts
+// Ramda
+head([1, 2, 3]);
+
+// Remeda
+first([1, 2, 3]);
+```
+
+### Strings
+
+```ts
+// Ramda
+head("abc");
+
+// Remeda
+sliceString("abc", 0, 1);
+```
diff --git a/docs/src/content/mapping/ramda/identity.md b/docs/src/content/mapping/ramda/identity.md
new file mode 100644
index 0000000..613abd3
--- /dev/null
+++ b/docs/src/content/mapping/ramda/identity.md
@@ -0,0 +1,15 @@
+---
+category: Function
+remeda: identity
+---
+
+In Remeda the `identity` function is a factory that creates an _identity
+function_; it needs to be called to be used.
+
+```ts
+// Ramda
+map(identity);
+
+// Remeda
+map(identity());
+```
diff --git a/docs/src/content/mapping/ramda/inc.md b/docs/src/content/mapping/ramda/inc.md
new file mode 100644
index 0000000..640a740
--- /dev/null
+++ b/docs/src/content/mapping/ramda/inc.md
@@ -0,0 +1,14 @@
+---
+category: Math
+remeda: add
+---
+
+Use `add` with an operand of `1`.
+
+```ts
+// Ramda
+inc(value);
+
+// Remeda
+add(value, 1);
+```
diff --git a/docs/src/content/mapping/ramda/includes.md b/docs/src/content/mapping/ramda/includes.md
new file mode 100644
index 0000000..5d1d195
--- /dev/null
+++ b/docs/src/content/mapping/ramda/includes.md
@@ -0,0 +1,59 @@
+---
+category: List
+remeda: isIncludedIn
+---
+
+- In Ramda `includes` also works on strings; use the native JS
+  [`String.prototype.includes`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/includes)
+  for strings instead.
+- Ramda performs a deep equality check for the item, making `includes` useful
+  for array and object items too. In Remeda the comparison is performed via the
+  JS native [`Array.prototype.includes`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes).
+  For deep equality use the native [`Array.prototype.some`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some)
+  with Remeda's [`isDeepEqual`](/docs#isDeepEqual).
+
+### Arrays
+
+```ts
+// Ramda
+includes(1, [1, 2, 3]);
+
+// Remeda
+isIncludedIn(1, [1, 2, 3]);
+```
+
+### Strings
+
+```ts
+const DATA = "Hello, World!";
+
+// Remeda
+includes("lo", DATA);
+
+// Native
+DATA.includes("lo");
+```
+
+### Object items
+
+```ts
+const DATA = [{ name: "Fred" }];
+
+// Ramda
+includes({ name: "Fred" }, DATA);
+
+// Remeda
+DATA.some(isDeepEqual({ name: "Fred" }));
+```
+
+### Array items
+
+```ts
+const DATA = [[42]];
+
+// Ramda
+includes([42], DATA);
+
+// Remeda
+DATA.some(isDeepEqual([42]));
+```
diff --git a/docs/src/content/mapping/ramda/indexBy.md b/docs/src/content/mapping/ramda/indexBy.md
new file mode 100644
index 0000000..1d76630
--- /dev/null
+++ b/docs/src/content/mapping/ramda/indexBy.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: indexBy
+---
diff --git a/docs/src/content/mapping/ramda/init.md b/docs/src/content/mapping/ramda/init.md
new file mode 100644
index 0000000..a8535df
--- /dev/null
+++ b/docs/src/content/mapping/ramda/init.md
@@ -0,0 +1,27 @@
+---
+category: List
+remeda: dropLast
+---
+
+- Equivalent to `dropLast` with an argument of `1`.
+- On strings use [`sliceString`](/docs#sliceString) with `0` and `-1` as arguments instead.
+
+### Arrays
+
+```ts
+// Ramda
+init([1, 2, 3]);
+
+// Remeda
+dropLast([1, 2, 3], 1);
+```
+
+### Strings
+
+```ts
+// Ramda
+init("abc");
+
+// Remeda
+sliceString("abc", 0, -1);
+```
diff --git a/docs/src/content/mapping/ramda/innerJoin.md b/docs/src/content/mapping/ramda/innerJoin.md
new file mode 100644
index 0000000..09764ac
--- /dev/null
+++ b/docs/src/content/mapping/ramda/innerJoin.md
@@ -0,0 +1,4 @@
+---
+category: Relation
+remeda: intersectionWith
+---
diff --git a/docs/src/content/mapping/ramda/intersection.md b/docs/src/content/mapping/ramda/intersection.md
new file mode 100644
index 0000000..96815ee
--- /dev/null
+++ b/docs/src/content/mapping/ramda/intersection.md
@@ -0,0 +1,4 @@
+---
+category: Relation
+remeda: intersection
+---
diff --git a/docs/src/content/mapping/ramda/invertObj.md b/docs/src/content/mapping/ramda/invertObj.md
new file mode 100644
index 0000000..9421615
--- /dev/null
+++ b/docs/src/content/mapping/ramda/invertObj.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: invert
+---
diff --git a/docs/src/content/mapping/ramda/isEmpty.md b/docs/src/content/mapping/ramda/isEmpty.md
new file mode 100644
index 0000000..b6af793
--- /dev/null
+++ b/docs/src/content/mapping/ramda/isEmpty.md
@@ -0,0 +1,4 @@
+---
+category: Logic
+remeda: isEmpty
+---
diff --git a/docs/src/content/mapping/ramda/isNil.md b/docs/src/content/mapping/ramda/isNil.md
new file mode 100644
index 0000000..1b24e58
--- /dev/null
+++ b/docs/src/content/mapping/ramda/isNil.md
@@ -0,0 +1,4 @@
+---
+category: Type
+remeda: isNullish
+---
diff --git a/docs/src/content/mapping/ramda/isNotNil.md b/docs/src/content/mapping/ramda/isNotNil.md
new file mode 100644
index 0000000..c7c5acb
--- /dev/null
+++ b/docs/src/content/mapping/ramda/isNotNil.md
@@ -0,0 +1,4 @@
+---
+category: Type
+remeda: isNonNullish
+---
diff --git a/docs/src/content/mapping/ramda/join.md b/docs/src/content/mapping/ramda/join.md
new file mode 100644
index 0000000..117bfd1
--- /dev/null
+++ b/docs/src/content/mapping/ramda/join.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: join
+---
diff --git a/docs/src/content/mapping/ramda/keys.md b/docs/src/content/mapping/ramda/keys.md
new file mode 100644
index 0000000..6cc45a0
--- /dev/null
+++ b/docs/src/content/mapping/ramda/keys.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: keys
+---
diff --git a/docs/src/content/mapping/ramda/last.md b/docs/src/content/mapping/ramda/last.md
new file mode 100644
index 0000000..1da71b6
--- /dev/null
+++ b/docs/src/content/mapping/ramda/last.md
@@ -0,0 +1,26 @@
+---
+category: List
+remeda: last
+---
+
+For strings use [`sliceString`](/docs#sliceString) with `-1` as an argument instead.
+
+### Arrays
+
+```ts
+// Ramda
+last([1, 2, 3]);
+
+// Remeda
+last([1, 2, 3]);
+```
+
+### Strings
+
+```ts
+// Ramda
+last("abc");
+
+// Remeda
+sliceString("abc", -1);
+```
diff --git a/docs/src/content/mapping/ramda/length.md b/docs/src/content/mapping/ramda/length.md
new file mode 100644
index 0000000..d0e1a45
--- /dev/null
+++ b/docs/src/content/mapping/ramda/length.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: length
+---
diff --git a/docs/src/content/mapping/ramda/map.md b/docs/src/content/mapping/ramda/map.md
new file mode 100644
index 0000000..17d8bc9
--- /dev/null
+++ b/docs/src/content/mapping/ramda/map.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: map
+---
diff --git a/docs/src/content/mapping/ramda/mapAccum.md b/docs/src/content/mapping/ramda/mapAccum.md
new file mode 100644
index 0000000..722df38
--- /dev/null
+++ b/docs/src/content/mapping/ramda/mapAccum.md
@@ -0,0 +1,23 @@
+---
+category: List
+remeda: mapWithFeedback
+---
+
+Remeda's `mapWithFeedback` has some differences from Ramda's `mapAccum`, but
+could be used to achieve the same results:
+
+- The mapper function only returns a single value for both the accumulator and
+  the value (like `reduce` does). `mapWithFeedback` would not work if you need
+  these to have different values.
+- Only the accumulated array is returned instead of a tuple. The final result of
+  the computation would always be the last element in this list, and could be
+  retrieved using [`last`](/docs#last).
+
+```ts
+// Ramda
+const result = mapAccum((a, b) => [a + b, a + b], 0, ["1", "2", "3", "4"]);
+
+// Remeda
+const temp = mapWithFeedback(["1", "2", "3", "4"], (a, b) => a + b, 0);
+const result = [last(temp), temp];
+```
diff --git a/docs/src/content/mapping/ramda/mapObjIndexed.md b/docs/src/content/mapping/ramda/mapObjIndexed.md
new file mode 100644
index 0000000..442e473
--- /dev/null
+++ b/docs/src/content/mapping/ramda/mapObjIndexed.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: mapValues
+---
diff --git a/docs/src/content/mapping/ramda/max.md b/docs/src/content/mapping/ramda/max.md
new file mode 100644
index 0000000..960e03d
--- /dev/null
+++ b/docs/src/content/mapping/ramda/max.md
@@ -0,0 +1,21 @@
+---
+category: Relation
+---
+
+_Not provided by Remeda._
+
+The Ramda `max` function takes exactly **2** arguments. It is easily replicated
+using native JS operators.
+
+```ts
+// Ramda
+max(a, b);
+
+// Curried
+const maxA = max(a);
+
+// Native
+a > b ? a : b;
+
+const maxA = (b: number) => (a > b ? a : b);
+```
diff --git a/docs/src/content/mapping/ramda/maxBy.md b/docs/src/content/mapping/ramda/maxBy.md
new file mode 100644
index 0000000..9830507
--- /dev/null
+++ b/docs/src/content/mapping/ramda/maxBy.md
@@ -0,0 +1,17 @@
+---
+category: Relation
+remeda: firstBy
+---
+
+The Remeda `firstBy` returns what would be the first item in an array if it was
+sorted by the order criteria (without actually sorting the array). Ramda's
+`maxBy` could be rebuilt using `firstBy` by taking the mapping function in
+descending order, and wrapping the 2 arguments with an array.
+
+```ts
+// Ramda
+maxBy(mapperFunc, a, b);
+
+// Remeda
+firstBy([a, b], [mapperFunc, "desc"]);
+```
diff --git a/docs/src/content/mapping/ramda/mean.md b/docs/src/content/mapping/ramda/mean.md
new file mode 100644
index 0000000..3a88b5d
--- /dev/null
+++ b/docs/src/content/mapping/ramda/mean.md
@@ -0,0 +1,14 @@
+---
+category: Math
+remeda: meanBy
+---
+
+Use `meanBy` with `identity` as the mapper.
+
+```ts
+// Ramda
+mean(values);
+
+// Remeda
+meanBy(values, identity());
+```
diff --git a/docs/src/content/mapping/ramda/mergeAll.md b/docs/src/content/mapping/ramda/mergeAll.md
new file mode 100644
index 0000000..74038db
--- /dev/null
+++ b/docs/src/content/mapping/ramda/mergeAll.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: mergeAll
+---
diff --git a/docs/src/content/mapping/ramda/mergeDeepRight.md b/docs/src/content/mapping/ramda/mergeDeepRight.md
new file mode 100644
index 0000000..b4cbc8c
--- /dev/null
+++ b/docs/src/content/mapping/ramda/mergeDeepRight.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: mergeDeep
+---
diff --git a/docs/src/content/mapping/ramda/mergeRight.md b/docs/src/content/mapping/ramda/mergeRight.md
new file mode 100644
index 0000000..0eabecd
--- /dev/null
+++ b/docs/src/content/mapping/ramda/mergeRight.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: merge
+---
diff --git a/docs/src/content/mapping/ramda/min.md b/docs/src/content/mapping/ramda/min.md
new file mode 100644
index 0000000..b746420
--- /dev/null
+++ b/docs/src/content/mapping/ramda/min.md
@@ -0,0 +1,21 @@
+---
+category: Relation
+---
+
+_Not provided by Remeda._
+
+The Ramda `max` function takes exactly **2** arguments. It is easily replicated
+using native JS operators.
+
+```ts
+// Ramda
+min(a, b);
+
+// Curried
+const minA = min(a);
+
+// Native
+a < b ? a : b;
+
+const minA = (b: number) => (a < b ? a : b);
+```
diff --git a/docs/src/content/mapping/ramda/minBy.md b/docs/src/content/mapping/ramda/minBy.md
new file mode 100644
index 0000000..d996d71
--- /dev/null
+++ b/docs/src/content/mapping/ramda/minBy.md
@@ -0,0 +1,17 @@
+---
+category: Relation
+remeda: firstBy
+---
+
+The Remeda `firstBy` returns what would be the first item in an array if it was
+sorted by the order criteria (without actually sorting the array). Ramda's
+`minBy` could be rebuilt using `firstBy` by taking the mapping function in, and
+wrapping the 2 arguments with an array.
+
+```ts
+// Ramda
+minBy(mapperFunc, a, b);
+
+// Remeda
+firstBy([a, b], mapperFunc);
+```
diff --git a/docs/src/content/mapping/ramda/multiply.md b/docs/src/content/mapping/ramda/multiply.md
new file mode 100644
index 0000000..dfea7e6
--- /dev/null
+++ b/docs/src/content/mapping/ramda/multiply.md
@@ -0,0 +1,4 @@
+---
+category: Math
+remeda: multiply
+---
diff --git a/docs/src/content/mapping/ramda/negate.md b/docs/src/content/mapping/ramda/negate.md
new file mode 100644
index 0000000..39e5a03
--- /dev/null
+++ b/docs/src/content/mapping/ramda/negate.md
@@ -0,0 +1,14 @@
+---
+category: Math
+remeda: multiply
+---
+
+Use `multiply` with an operand of `-1`.
+
+```ts
+// Ramda
+negate(value);
+
+// Remeda
+multiply(value, -1);
+```
diff --git a/docs/src/content/mapping/ramda/not.md b/docs/src/content/mapping/ramda/not.md
new file mode 100644
index 0000000..6418b9b
--- /dev/null
+++ b/docs/src/content/mapping/ramda/not.md
@@ -0,0 +1,27 @@
+---
+category: Logic
+remeda: isNot
+---
+
+The function only accepts boolean values, to support arbitrary values compose
+it with [`isTruthy`](/docs#isTruthy).
+
+### Booleans
+
+```ts
+// Ramda
+not(val);
+
+// Remeda
+isNot(val);
+```
+
+### Arbitrary
+
+```ts
+// Ramda
+not(val);
+
+// Remeda
+isNot(isTruthy(val));
+```
diff --git a/docs/src/content/mapping/ramda/nth.md b/docs/src/content/mapping/ramda/nth.md
new file mode 100644
index 0000000..9d72ef6
--- /dev/null
+++ b/docs/src/content/mapping/ramda/nth.md
@@ -0,0 +1,37 @@
+---
+category: List
+---
+
+_Not provided by Remeda._
+
+- For index `0` use [`first`](/docs#first).
+- For index `-1` use [`last`](/docs#last).
+- For arbitrary _non-negative_ indices use the native JS `data[n]`.
+- Or use [`Array.prototype.at`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at) for any index.
+
+```ts
+// Ramda
+nth(0, DATA);
+
+// Remeda
+first(DATA);
+
+// Ramda
+nth(1, DATA);
+
+// Native
+DATA[1];
+DATA.at(1);
+
+// Ramda
+nth(-1, DATA);
+
+// Remeda
+last(DATA);
+
+// Ramda
+nth(-2, DATA);
+
+// Native
+DATA.at(-2);
+```
diff --git a/docs/src/content/mapping/ramda/objOf.md b/docs/src/content/mapping/ramda/objOf.md
new file mode 100644
index 0000000..1d87517
--- /dev/null
+++ b/docs/src/content/mapping/ramda/objOf.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: objOf
+---
diff --git a/docs/src/content/mapping/ramda/omit.md b/docs/src/content/mapping/ramda/omit.md
new file mode 100644
index 0000000..0158a36
--- /dev/null
+++ b/docs/src/content/mapping/ramda/omit.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: omit
+---
diff --git a/docs/src/content/mapping/ramda/once.md b/docs/src/content/mapping/ramda/once.md
new file mode 100644
index 0000000..42168dc
--- /dev/null
+++ b/docs/src/content/mapping/ramda/once.md
@@ -0,0 +1,4 @@
+---
+category: Function
+remeda: once
+---
diff --git a/docs/src/content/mapping/ramda/partition.md b/docs/src/content/mapping/ramda/partition.md
new file mode 100644
index 0000000..5222ed8
--- /dev/null
+++ b/docs/src/content/mapping/ramda/partition.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: partition
+---
diff --git a/docs/src/content/mapping/ramda/pathOr.md b/docs/src/content/mapping/ramda/pathOr.md
new file mode 100644
index 0000000..03d83c6
--- /dev/null
+++ b/docs/src/content/mapping/ramda/pathOr.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: pathOr
+---
diff --git a/docs/src/content/mapping/ramda/pick.md b/docs/src/content/mapping/ramda/pick.md
new file mode 100644
index 0000000..a6f2e5b
--- /dev/null
+++ b/docs/src/content/mapping/ramda/pick.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: pick
+---
diff --git a/docs/src/content/mapping/ramda/pickBy.md b/docs/src/content/mapping/ramda/pickBy.md
new file mode 100644
index 0000000..0ab0cd1
--- /dev/null
+++ b/docs/src/content/mapping/ramda/pickBy.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: pickBy
+---
diff --git a/docs/src/content/mapping/ramda/pipe.md b/docs/src/content/mapping/ramda/pipe.md
new file mode 100644
index 0000000..573407b
--- /dev/null
+++ b/docs/src/content/mapping/ramda/pipe.md
@@ -0,0 +1,4 @@
+---
+category: Function
+remeda: pipe
+---
diff --git a/docs/src/content/mapping/ramda/product.md b/docs/src/content/mapping/ramda/product.md
new file mode 100644
index 0000000..85b37c3
--- /dev/null
+++ b/docs/src/content/mapping/ramda/product.md
@@ -0,0 +1,4 @@
+---
+category: Math
+remeda: product
+---
diff --git a/docs/src/content/mapping/ramda/prop.md b/docs/src/content/mapping/ramda/prop.md
new file mode 100644
index 0000000..6057ce0
--- /dev/null
+++ b/docs/src/content/mapping/ramda/prop.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: prop
+---
diff --git a/docs/src/content/mapping/ramda/range.md b/docs/src/content/mapping/ramda/range.md
new file mode 100644
index 0000000..ec585d3
--- /dev/null
+++ b/docs/src/content/mapping/ramda/range.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: range
+---
diff --git a/docs/src/content/mapping/ramda/reduce.md b/docs/src/content/mapping/ramda/reduce.md
new file mode 100644
index 0000000..036c5ee
--- /dev/null
+++ b/docs/src/content/mapping/ramda/reduce.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: reduce
+---
diff --git a/docs/src/content/mapping/ramda/reject.md b/docs/src/content/mapping/ramda/reject.md
new file mode 100644
index 0000000..3ff9335
--- /dev/null
+++ b/docs/src/content/mapping/ramda/reject.md
@@ -0,0 +1,17 @@
+---
+category: List
+remeda: filter
+---
+
+Wrap the callback with [`isNot`](/docs#isNot).
+
+```ts
+// Ramda
+reject(predicate, DATA);
+
+// Remeda
+filter(DATA, isNot(predicate));
+
+// Or in a pipe
+pipe(DATA, filter(isNot(predicate)));
+```
diff --git a/docs/src/content/mapping/ramda/remove.md b/docs/src/content/mapping/ramda/remove.md
new file mode 100644
index 0000000..20343f3
--- /dev/null
+++ b/docs/src/content/mapping/ramda/remove.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: splice
+---
diff --git a/docs/src/content/mapping/ramda/reverse.md b/docs/src/content/mapping/ramda/reverse.md
new file mode 100644
index 0000000..720d084
--- /dev/null
+++ b/docs/src/content/mapping/ramda/reverse.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: reverse
+---
diff --git a/docs/src/content/mapping/ramda/set.md b/docs/src/content/mapping/ramda/set.md
new file mode 100644
index 0000000..68d6c99
--- /dev/null
+++ b/docs/src/content/mapping/ramda/set.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: set
+---
diff --git a/docs/src/content/mapping/ramda/sort.md b/docs/src/content/mapping/ramda/sort.md
new file mode 100644
index 0000000..1e18f68
--- /dev/null
+++ b/docs/src/content/mapping/ramda/sort.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: sort
+---
diff --git a/docs/src/content/mapping/ramda/sortBy.md b/docs/src/content/mapping/ramda/sortBy.md
new file mode 100644
index 0000000..b06b47f
--- /dev/null
+++ b/docs/src/content/mapping/ramda/sortBy.md
@@ -0,0 +1,4 @@
+---
+category: Relation
+remeda: sortBy
+---
diff --git a/docs/src/content/mapping/ramda/sortWith.md b/docs/src/content/mapping/ramda/sortWith.md
new file mode 100644
index 0000000..f23ba39
--- /dev/null
+++ b/docs/src/content/mapping/ramda/sortWith.md
@@ -0,0 +1,35 @@
+---
+category: Relation
+remeda: sortBy
+---
+
+Remeda's `sortBy` also covers the use-case of `sortWith` using a simpler syntax
+to describe complex sorting logic; the function takes a variadic list of
+arguments instead of an array, and the sorting direction (ascending/descending)
+is determined by an optional tuple syntax for the operator.
+
+```ts
+const DATA = [
+  {
+    name: "clara",
+    age: 40,
+  },
+  {
+    name: "bob",
+    age: 30,
+  },
+  {
+    name: "alice",
+    age: 40,
+  },
+];
+
+// Ramda
+sortWith([descend(prop("age")), ascend(prop("name"))])(DATA);
+
+// Remeda
+sortBy(DATA, [prop("age"), "desc"], prop("name"));
+
+// Or in a pipe
+pipe(DATA, sortBy([prop("age"), "desc"], prop("name")));
+```
diff --git a/docs/src/content/mapping/ramda/split.md b/docs/src/content/mapping/ramda/split.md
new file mode 100644
index 0000000..d37386c
--- /dev/null
+++ b/docs/src/content/mapping/ramda/split.md
@@ -0,0 +1,4 @@
+---
+category: String
+remeda: split
+---
diff --git a/docs/src/content/mapping/ramda/splitAt.md b/docs/src/content/mapping/ramda/splitAt.md
new file mode 100644
index 0000000..751e7da
--- /dev/null
+++ b/docs/src/content/mapping/ramda/splitAt.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: splitAt
+---
diff --git a/docs/src/content/mapping/ramda/splitWhen.md b/docs/src/content/mapping/ramda/splitWhen.md
new file mode 100644
index 0000000..b760a2c
--- /dev/null
+++ b/docs/src/content/mapping/ramda/splitWhen.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: splitWhen
+---
diff --git a/docs/src/content/mapping/ramda/subtract.md b/docs/src/content/mapping/ramda/subtract.md
new file mode 100644
index 0000000..851dea4
--- /dev/null
+++ b/docs/src/content/mapping/ramda/subtract.md
@@ -0,0 +1,4 @@
+---
+category: Math
+remeda: subtract
+---
diff --git a/docs/src/content/mapping/ramda/sum.md b/docs/src/content/mapping/ramda/sum.md
new file mode 100644
index 0000000..46e0d4f
--- /dev/null
+++ b/docs/src/content/mapping/ramda/sum.md
@@ -0,0 +1,4 @@
+---
+category: Math
+remeda: sum
+---
diff --git a/docs/src/content/mapping/ramda/swap.md b/docs/src/content/mapping/ramda/swap.md
new file mode 100644
index 0000000..f962cc0
--- /dev/null
+++ b/docs/src/content/mapping/ramda/swap.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: swapIndices
+---
diff --git a/docs/src/content/mapping/ramda/tail.md b/docs/src/content/mapping/ramda/tail.md
new file mode 100644
index 0000000..fa6cde3
--- /dev/null
+++ b/docs/src/content/mapping/ramda/tail.md
@@ -0,0 +1,27 @@
+---
+category: List
+remeda: drop
+---
+
+- Equivalent to `drop` with an argument of `1`.
+- On strings use [`sliceString`](/docs#sliceString) instead.
+
+### Arrays
+
+```ts
+// Ramda
+tail([1, 2, 3]);
+
+// Remeda
+drop([1, 2, 3], 1);
+```
+
+### Strings
+
+```ts
+// Ramda
+tail("abc");
+
+// Remeda
+sliceString("abc", 1);
+```
diff --git a/docs/src/content/mapping/ramda/take.md b/docs/src/content/mapping/ramda/take.md
new file mode 100644
index 0000000..a33bed4
--- /dev/null
+++ b/docs/src/content/mapping/ramda/take.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: take
+---
diff --git a/docs/src/content/mapping/ramda/takeLast.md b/docs/src/content/mapping/ramda/takeLast.md
new file mode 100644
index 0000000..6d55cef
--- /dev/null
+++ b/docs/src/content/mapping/ramda/takeLast.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: takeLast
+---
diff --git a/docs/src/content/mapping/ramda/takeLastWhile.md b/docs/src/content/mapping/ramda/takeLastWhile.md
new file mode 100644
index 0000000..5ae16be
--- /dev/null
+++ b/docs/src/content/mapping/ramda/takeLastWhile.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: takeLastWhile
+---
diff --git a/docs/src/content/mapping/ramda/takeWhile.md b/docs/src/content/mapping/ramda/takeWhile.md
new file mode 100644
index 0000000..7c109b8
--- /dev/null
+++ b/docs/src/content/mapping/ramda/takeWhile.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: takeWhile
+---
diff --git a/docs/src/content/mapping/ramda/tap.md b/docs/src/content/mapping/ramda/tap.md
new file mode 100644
index 0000000..adc10bd
--- /dev/null
+++ b/docs/src/content/mapping/ramda/tap.md
@@ -0,0 +1,4 @@
+---
+category: Function
+remeda: tap
+---
diff --git a/docs/src/content/mapping/ramda/times.md b/docs/src/content/mapping/ramda/times.md
new file mode 100644
index 0000000..c93754e
--- /dev/null
+++ b/docs/src/content/mapping/ramda/times.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: times
+---
diff --git a/docs/src/content/mapping/ramda/toPairs.md b/docs/src/content/mapping/ramda/toPairs.md
new file mode 100644
index 0000000..1f8503a
--- /dev/null
+++ b/docs/src/content/mapping/ramda/toPairs.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: entries
+---
diff --git a/docs/src/content/mapping/ramda/uniq.md b/docs/src/content/mapping/ramda/uniq.md
new file mode 100644
index 0000000..f4bbfa2
--- /dev/null
+++ b/docs/src/content/mapping/ramda/uniq.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: unique
+---
diff --git a/docs/src/content/mapping/ramda/uniqBy.md b/docs/src/content/mapping/ramda/uniqBy.md
new file mode 100644
index 0000000..43be2e7
--- /dev/null
+++ b/docs/src/content/mapping/ramda/uniqBy.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: uniqueBy
+---
diff --git a/docs/src/content/mapping/ramda/uniqWith.md b/docs/src/content/mapping/ramda/uniqWith.md
new file mode 100644
index 0000000..f42b83b
--- /dev/null
+++ b/docs/src/content/mapping/ramda/uniqWith.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: uniqueWith
+---
diff --git a/docs/src/content/mapping/ramda/values.md b/docs/src/content/mapping/ramda/values.md
new file mode 100644
index 0000000..26e50fa
--- /dev/null
+++ b/docs/src/content/mapping/ramda/values.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: values
+---
diff --git a/docs/src/content/mapping/ramda/where.md b/docs/src/content/mapping/ramda/where.md
new file mode 100644
index 0000000..025b91e
--- /dev/null
+++ b/docs/src/content/mapping/ramda/where.md
@@ -0,0 +1,4 @@
+---
+category: Object
+remeda: hasSubObject
+---
diff --git a/docs/src/content/mapping/ramda/zip.md b/docs/src/content/mapping/ramda/zip.md
new file mode 100644
index 0000000..5827772
--- /dev/null
+++ b/docs/src/content/mapping/ramda/zip.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: zip
+---
diff --git a/docs/src/content/mapping/ramda/zipObj.md b/docs/src/content/mapping/ramda/zipObj.md
new file mode 100644
index 0000000..3141e23
--- /dev/null
+++ b/docs/src/content/mapping/ramda/zipObj.md
@@ -0,0 +1,15 @@
+---
+category: List
+---
+
+_Not provided by Remeda._
+
+Compose [`zip`](/doc#zip) and [`fromEntries`](/doc#fromEntries):
+
+```ts
+// Ramda
+zipObj(keys, values);
+
+// Remeda
+fromEntries(zip(keys, values));
+```
diff --git a/docs/src/content/mapping/ramda/zipWith.md b/docs/src/content/mapping/ramda/zipWith.md
new file mode 100644
index 0000000..f36bd16
--- /dev/null
+++ b/docs/src/content/mapping/ramda/zipWith.md
@@ -0,0 +1,4 @@
+---
+category: List
+remeda: zipWith
+---
diff --git a/docs/src/layouts/docs-page.astro b/docs/src/layouts/docs-page.astro
index 9c098e0..a8a652b 100644
--- a/docs/src/layouts/docs-page.astro
+++ b/docs/src/layouts/docs-page.astro
@@ -13,9 +13,10 @@ import BaseLayout from "./base.astro";
 type Props = {
   readonly categorized: CategorizedFunctions;
   readonly showVersionSelector?: boolean;
+  readonly title?: string;
 };
 
-const { categorized, showVersionSelector = false } = Astro.props;
+const { categorized, showVersionSelector = false, title } = Astro.props;
 
 const { pathname } = Astro.url;
 
@@ -40,6 +41,11 @@ const renderedArticles = await pipe(
       class="sticky top-24 hidden h-[calc(100dvh-8rem)] w-64 overscroll-contain md:block"
     >
       <Navbar entries={navbarEntries} client:load>
+        {
+          title !== undefined && (
+            <h2 class="text-lg font-bold capitalize">{title}</h2>
+          )
+        }
         {
           showVersionSelector && (
             <VersionSelector pathname={pathname} client:load />
diff --git a/docs/src/lib/mappings.ts b/docs/src/lib/mappings.ts
index 60c2dff..82f2449 100644
--- a/docs/src/lib/mappings.ts
+++ b/docs/src/lib/mappings.ts
@@ -1,13 +1,52 @@
 import { getCollection } from "astro:content";
-import { pipe, groupBy, mapValues, map, pick, piped, prop } from "remeda";
+import {
+  first,
+  groupBy,
+  map,
+  mapValues,
+  pick,
+  pipe,
+  piped,
+  prop,
+  sortBy,
+  unique,
+} from "remeda";
+import type { Split } from "type-fest";
 
 const COLLECTION = "mapping";
 
+export type Library = Awaited<ReturnType<typeof getLibraries>>[number];
+
+/**
+ * We use the collection itself as the source-of-truth for what libraries we
+ * offer migration guides for. As long as there's a directory for a mapping, we
+ * will consider that mapping to be available.
+ *
+ * @returns the list of all libraries with mappings in the collection.
+ */
+export const getLibraries = async () =>
+  pipe(
+    await getCollection(COLLECTION),
+    map(
+      piped(
+        prop("id"),
+        // TODO: Add split to remeda!
+        ($) => $.split("/") as Split<typeof $, "/">,
+        first(),
+      ),
+    ),
+    unique(),
+  );
+
 export async function getMappingEntries(library: string) {
   const fileNameRegExp = new RegExp(`^${library}/(?<name>.*).mdx?$`, "ui");
 
   return pipe(
     await getCollection(COLLECTION, ({ id }) => id.startsWith(library)),
+    // The files will be sorted by whatever linux considers for ordering, but
+    // that makes uppercase and lowercase letters be separated. We want to use
+    // a regular "dictionary" style order.
+    sortBy(({ id }) => id.toLocaleLowerCase()),
     groupBy(({ data: { category } }) => category),
     mapValues(
       map(
@@ -22,12 +61,12 @@ export async function getMappingEntries(library: string) {
   );
 }
 
-export function mappingUrl(library: string, name: string): string | undefined {
+export function mappingUrl(library: Library, name: string): string {
   switch (library) {
     case "lodash":
       return `https://lodash.com/docs/4.17.15#${name}`;
 
-    default:
-      return;
+    case "ramda":
+      return `https://ramdajs.com/docs/#${name}`;
   }
 }
diff --git a/docs/src/pages/index.mdx b/docs/src/pages/index.mdx
index 11ba630..6dd2640 100644
--- a/docs/src/pages/index.mdx
+++ b/docs/src/pages/index.mdx
@@ -10,7 +10,7 @@ Fully written in TypeScript for powerful, type-safe, and functional utilities.
 
 <InstallCommands />
 
-## 🧰 Expansive
+## 🧰 Useful
 
 Remeda offers a wide range of common everyday utility functions so that you
 don't have to deal with the minutiae of writing them yourself. We ensure that
diff --git a/docs/src/pages/migrate/[library].astro b/docs/src/pages/migrate/[library].astro
new file mode 100644
index 0000000..0b95fe5
--- /dev/null
+++ b/docs/src/pages/migrate/[library].astro
@@ -0,0 +1,28 @@
+---
+import MappingFunctionCard from "@/components/mapping-function-card.astro";
+import DocsPage from "@/layouts/docs-page.astro";
+import { getLibraries, getMappingEntries } from "@/lib/mappings";
+import { map, entries, objOf, flatMap, pipe, sortBy, piped } from "remeda";
+
+const { library } = Astro.params;
+
+const mappingEntries = await getMappingEntries(library);
+
+const forDisplay = pipe(
+  mappingEntries,
+  entries(),
+  sortBy(([category]) => category),
+  flatMap(([, functions]) => functions),
+);
+
+export const getStaticPaths = async () =>
+  map(await getLibraries(), piped(objOf("library"), objOf("params")));
+---
+
+<DocsPage categorized={mappingEntries} title={library}>
+  {
+    forDisplay.map(({ name }) => (
+      <MappingFunctionCard name={name} library={library} />
+    ))
+  }
+</DocsPage>
diff --git a/docs/src/pages/migrate/index.md b/docs/src/pages/migrate/index.md
deleted file mode 100644
index acb2e8c..0000000
--- a/docs/src/pages/migrate/index.md
+++ /dev/null
@@ -1,13 +0,0 @@
----
-layout: ../../layouts/wiki.astro
----
-
-# Migration Guides
-
-If your project is already using a _different_ library for utility functions you
-can follow the function-by-function instructions in the migration guides below
-provided for popular libraries. If you need help with a different library reach
-out on our GitHub page to let us know.
-
-- [Lodash](/migrate/lodash)
-- [Ramda](/migrate/ramda)
diff --git a/docs/src/pages/migrate/lodash.astro b/docs/src/pages/migrate/lodash.astro
deleted file mode 100644
index db62ae1..0000000
--- a/docs/src/pages/migrate/lodash.astro
+++ /dev/null
@@ -1,25 +0,0 @@
----
-import MappingFunctionCard from "@/components/mapping-function-card.astro";
-import DocsPage from "@/layouts/docs-page.astro";
-import { getMappingEntries } from "@/lib/mappings";
-import { entries, flatMap, pipe, sortBy } from "remeda";
-
-const LIBRARY = "lodash";
-
-const mappingEntries = await getMappingEntries(LIBRARY);
-
-const forDisplay = pipe(
-  mappingEntries,
-  entries(),
-  sortBy(([category]) => category),
-  flatMap(([, functions]) => functions),
-);
----
-
-<DocsPage categorized={mappingEntries}>
-  {
-    forDisplay.map(({ name }) => (
-      <MappingFunctionCard name={name} library={LIBRARY} />
-    ))
-  }
-</DocsPage>
diff --git a/docs/src/pages/migrate/ramda.md b/docs/src/pages/migrate/ramda.md
deleted file mode 100644
index 177af8e..0000000
--- a/docs/src/pages/migrate/ramda.md
+++ /dev/null
@@ -1,112 +0,0 @@
----
-layout: ../../layouts/wiki.astro
----
-
-# Mapping for Ramda
-
-_Remeda functions are not necessarily drop-in replacements for the listed Ramda
-functions; they may behave differently. Be sure to consult Ramda's documentation
-when migrating._
-
-| Ramda                                                              | Remeda                                                            |
-| ------------------------------------------------------------------ | ----------------------------------------------------------------- |
-| [`add`](https://ramdajs.com/docs/#add)                             | [`add`](https://remedajs.com/docs/#add)                           |
-| [`allPass`](https://ramdajs.com/docs/#allPass)                     | [`allPass`](https://remedajs.com/docs/#allPass)                   |
-| [`always`](https://ramdajs.com/docs/#always)                       | [`constant`](https://remedajs.com/docs/#constant)                 |
-| [`anyPass`](https://ramdajs.com/docs/#anyPass)                     | [`anyPass`](https://remedajs.com/docs/#anyPass)                   |
-| [`assoc`](https://ramdajs.com/docs/#assoc)                         | [`set`](https://remedajs.com/docs/#set)                           |
-| [`assocPath`](https://ramdajs.com/docs/#assocPath)                 | [`setPath`](https://remedajs.com/docs/#setPath)                   |
-| [`chain`](https://ramdajs.com/docs/#chain)                         | [`flatMap`](https://remedajs.com/docs/#flatMap)                   |
-| [`clamp`](https://ramdajs.com/docs/#clamp)                         | [`clamp`](https://remedajs.com/docs/#clamp)                       |
-| [`clone`](https://ramdajs.com/docs/#clone)                         | [`clone`](https://remedajs.com/docs/#clone)                       |
-| [`concat`](https://ramdajs.com/docs/#concat)                       | [`concat`](https://remedajs.com/docs/#concat)                     |
-| [`cond`](https://ramdajs.com/docs/#cond)                           | [`conditional`](https://remedajs.com/docs/#conditional)           |
-| [`difference`](https://ramdajs.com/docs/#difference)               | [`difference`](https://remedajs.com/docs/#difference)             |
-| [`differenceWith`](https://ramdajs.com/docs/#differenceWith)       | [`differenceWith`](https://remedajs.com/docs/#differenceWith)     |
-| [`divide`](https://ramdajs.com/docs/#divide)                       | [`divide`](https://remedajs.com/docs/#divide)                     |
-| [`drop`](https://ramdajs.com/docs/#drop)                           | [`drop`](https://remedajs.com/docs/#drop)                         |
-| [`dropLast`](https://ramdajs.com/docs/#dropLast)                   | [`dropLast`](https://remedajs.com/docs/#dropLast)                 |
-| [`dropLastWhile`](https://ramdajs.com/docs/#dropLastWhile)         | [`dropLastWhile`](https://remedajs.com/docs/#dropLastWhile)       |
-| [`dropWhile`](https://ramdajs.com/docs/#dropWhile)                 | [`dropWhile`](https://remedajs.com/docs/#dropWhile)               |
-| [`equals`](https://ramdajs.com/docs/#equals)                       | [`isDeepEqual`](https://remedajs.com/docs/#isDeepEqual)           |
-| [`evolve`](https://ramdajs.com/docs/#evolve)                       | [`evolve`](https://remedajs.com/docs/#evolve)                     |
-| [`filter`](https://ramdajs.com/docs/#filter)                       | [`filter`](https://remedajs.com/docs/#filter)                     |
-| [`find`](https://ramdajs.com/docs/#find)                           | [`find`](https://remedajs.com/docs/#find)                         |
-| [`findIndex`](https://ramdajs.com/docs/#findIndex)                 | [`findIndex`](https://remedajs.com/docs/#findIndex)               |
-| [`findLast`](https://ramdajs.com/docs/#findLast)                   | [`findLast`](https://remedajs.com/docs/#findLast)                 |
-| [`findLastIndex`](https://ramdajs.com/docs/#findLastIndex)         | [`findLastIndex`](https://remedajs.com/docs/#findLastIndex)       |
-| [`flatten`](https://ramdajs.com/docs/#flatten)                     | [`flat`](https://remedajs.com/docs/#flat)                         |
-| [`forEach`](https://ramdajs.com/docs/#forEach)                     | [`forEach`](https://remedajs.com/docs/#forEach)                   |
-| [`forEachObjIndexed`](https://ramdajs.com/docs/#forEachObjIndexed) | [`forEachObj`](https://remedajs.com/docs/#forEachObj)             |
-| [`groupBy`](https://ramdajs.com/docs/#groupBy)                     | [`groupBy`](https://remedajs.com/docs/#groupBy)                   |
-| [`head`](https://ramdajs.com/docs/#head)                           | [`first`](https://remedajs.com/docs/#first)                       |
-| [`identity`](https://ramdajs.com/docs/#identity)                   | [`identity`](https://remedajs.com/docs/#identity)                 |
-| [`indexBy`](https://ramdajs.com/docs/#indexBy)                     | [`indexBy`](https://remedajs.com/docs/#indexBy)                   |
-| [`innerJoin`](https://ramdajs.com/docs/#innerJoin)                 | [`intersectionWith`](https://remedajs.com/docs/#intersectionWith) |
-| [`intersection`](https://ramdajs.com/docs/#intersection)           | [`intersection`](https://remedajs.com/docs/#intersection)         |
-| [`invertObj`](https://ramdajs.com/docs/#invertObj)                 | [`invert`](https://remedajs.com/docs/#invert)                     |
-| [`isEmpty`](https://ramdajs.com/docs/#isEmpty)                     | [`isEmpty`](https://remedajs.com/docs/#isEmpty)                   |
-| [`isNil`](https://ramdajs.com/docs/#isNil)                         | [`isNullish`](https://remedajs.com/docs/#isNullish)               |
-| [`isNotNil`](https://ramdajs.com/docs/#isNotNil)                   | [`isNonNullish`](https://remedajs.com/docs/#isNonNullish)         |
-| [`join`](https://ramdajs.com/docs/#join)                           | [`join`](https://remedajs.com/docs/#join)                         |
-| [`last`](https://ramdajs.com/docs/#last)                           | [`last`](https://remedajs.com/docs/#last)                         |
-| [`length`](https://ramdajs.com/docs/#length)                       | [`length`](https://remedajs.com/docs/#length)                     |
-| [`map`](https://ramdajs.com/docs/#map)                             | [`map`](https://remedajs.com/docs/#map)                           |
-| [`mapObjIndexed`](https://ramdajs.com/docs/#mapObjIndexed)         | [`mapValues`](https://remedajs.com/docs/#mapValues)               |
-| [`merge`](https://ramdajs.com/docs/#merge)                         | [`merge`](https://remedajs.com/docs/#merge)                       |
-| [`mergeAll`](https://ramdajs.com/docs/#mergeAll)                   | [`mergeAll`](https://remedajs.com/docs/#mergeAll)                 |
-| [`mergeDeepRight`](https://ramdajs.com/docs/#mergeDeepRight)       | [`mergeDeep`](https://remedajs.com/docs/#mergeDeep)               |
-| [`multiply`](https://ramdajs.com/docs/#multiply)                   | [`multiply`](https://remedajs.com/docs/#multiply)                 |
-| [`objOf`](https://ramdajs.com/docs/#objOf)                         | [`objOf`](https://remedajs.com/docs/#objOf)                       |
-| [`omit`](https://ramdajs.com/docs/#omit)                           | [`omit`](https://remedajs.com/docs/#omit)                         |
-| [`once`](https://ramdajs.com/docs/#once)                           | [`once`](https://remedajs.com/docs/#once)                         |
-| [`partition`](https://ramdajs.com/docs/#partition)                 | [`partition`](https://remedajs.com/docs/#partition)               |
-| [`pathOr`](https://ramdajs.com/docs/#pathOr)                       | [`pathOr`](https://remedajs.com/docs/#pathOr)                     |
-| [`pick`](https://ramdajs.com/docs/#pick)                           | [`pick`](https://remedajs.com/docs/#pick)                         |
-| [`pickBy`](https://ramdajs.com/docs/#pickBy)                       | [`pickBy`](https://remedajs.com/docs/#pickBy)                     |
-| [`pipe`](https://ramdajs.com/docs/#pipe)                           | [`pipe`](https://remedajs.com/docs/#pipe)                         |
-| [`product`](https://ramdajs.com/docs/#product)                     | [`product`](https://remedajs.com/docs/#product)                   |
-| [`prop`](https://ramdajs.com/docs/#prop)                           | [`prop`](https://remedajs.com/docs/#prop)                         |
-| [`range`](https://ramdajs.com/docs/#range)                         | [`range`](https://remedajs.com/docs/#range)                       |
-| [`reduce`](https://ramdajs.com/docs/#reduce)                       | [`reduce`](https://remedajs.com/docs/#reduce)                     |
-| [`remove`](https://ramdajs.com/docs/#remove)                       | [`splice`](https://remedajs.com/docs/#splice)                     |
-| [`reverse`](https://ramdajs.com/docs/#reverse)                     | [`reverse`](https://remedajs.com/docs/#reverse)                   |
-| [`set`](https://ramdajs.com/docs/#set)                             | [`addProp`](https://remedajs.com/docs/#addProp)                   |
-| [`sort`](https://ramdajs.com/docs/#sort)                           | [`sort`](https://remedajs.com/docs/#sort)                         |
-| [`sortBy`](https://ramdajs.com/docs/#sortBy)                       | [`sortBy`](https://remedajs.com/docs/#sortBy)                     |
-| [`splitAt`](https://ramdajs.com/docs/#splitAt)                     | [`splitAt`](https://remedajs.com/docs/#splitAt)                   |
-| [`splitWhen`](https://ramdajs.com/docs/#splitWhen)                 | [`splitWhen`](https://remedajs.com/docs/#splitWhen)               |
-| [`subtract`](https://ramdajs.com/docs/#subtract)                   | [`subtract`](https://remedajs.com/docs/#subtract)                 |
-| [`sum`](https://ramdajs.com/docs/#sum)                             | [`sum`](https://remedajs.com/docs/#sum)                           |
-| [`swap`](https://ramdajs.com/docs/#swap)                           | [`swapIndices`](https://remedajs.com/docs/#swapIndices)           |
-| [`take`](https://ramdajs.com/docs/#take)                           | [`take`](https://remedajs.com/docs/#take)                         |
-| [`takeLast`](https://ramdajs.com/docs/#takeLast)                   | [`takeLast`](https://remedajs.com/docs/#takeLast)                 |
-| [`takeLastWhile`](https://ramdajs.com/docs/#takeLastWhile)         | [`takeLastWhile`](https://remedajs.com/docs/#takeLastWhile)       |
-| [`takeWhile`](https://ramdajs.com/docs/#takeWhile)                 | [`takeWhile`](https://remedajs.com/docs/#takeWhile)               |
-| [`tap`](https://ramdajs.com/docs/#tap)                             | [`tap`](https://remedajs.com/docs/#tap)                           |
-| [`times`](https://ramdajs.com/docs/#times)                         | [`times`](https://remedajs.com/docs/#times)                       |
-| [`toPairs`](https://ramdajs.com/docs/#toPairs)                     | [`entries`](https://remedajs.com/docs/#entries)                   |
-| [`uniq`](https://ramdajs.com/docs/#uniq)                           | [`unique`](https://remedajs.com/docs/#unique)                     |
-| [`uniqBy`](https://ramdajs.com/docs/#uniqBy)                       | [`uniqueBy`](https://remedajs.com/docs/#uniqueBy)                 |
-| [`uniqWith`](https://ramdajs.com/docs/#uniqWith)                   | [`uniqueWith`](https://remedajs.com/docs/#uniqueWith)             |
-| [`where`](https://ramdajs.com/docs/#where)                         | [`hasSubobject`](https://remedajs.com/docs/#hasSubobject)         |
-
-## Helpful one-liners
-
-Some ramda functions don't have a Remeda equivalent, but can be easily replaced
-with a one-liner in TypeScript. Some of the most common are listed below.
-
-_TypeScript one-liners are not necessarily drop-in replacements that provide the
-exact functionality of the listedRamda functions. Be sure to consult Ramda's
-documentation and to check what features you are relying on when migrating._
-
-| Ramda    | Remeda                                      |
-| -------- | ------------------------------------------- |
-| `append` | `(arr, val) => [...arr, val]`               |
-| `max`    | `R.firstBy([R.identity, "desc"])`           |
-| `maxBy`  | `R.firstBy([fn, "desc"])`                   |
-| `min`    | `R.firstBy(R.identity)`                     |
-| `minBy`  | `R.firstBy(fn)`                             |
-| `nth`    | `n => a[n]`                                 |
-| `reject` | `R.filter(R.isNot(fn))`                     |
-| `zipObj` | `R.fromEntries.strict(R.zip(keys, values))` |
diff --git a/src/fromKeys.ts b/src/fromKeys.ts
index bf6cdac..9c196a5 100644
--- a/src/fromKeys.ts
+++ b/src/fromKeys.ts
@@ -1,5 +1,5 @@
 import type { Simplify } from "type-fest";
-import type { IterableContainer } from "./internal/types";
+import type { ExactRecord, IterableContainer } from "./internal/types";
 import { purry } from "./purry";
 
 // Takes a union of literals and creates a union of records with the value V for
@@ -13,7 +13,7 @@ type FromKeys<T extends IterableContainer, V> = T extends readonly []
   : T extends readonly [infer Head, ...infer Rest]
     ? ExactlyOneKey<Head, V> & FromKeys<Rest, V>
     : T[number] extends PropertyKey
-      ? Partial<Record<T[number], V>>
+      ? ExactRecord<T[number], V>
       : never;
 
 /**
diff --git a/src/hasSubObject.ts b/src/hasSubObject.ts
index e5c9533..298ec3e 100644
--- a/src/hasSubObject.ts
+++ b/src/hasSubObject.ts
@@ -9,6 +9,56 @@ type HasSubObjectGuard<T, S> = Simplify<
   Branded<S & T, typeof HAS_SUB_OBJECT_BRAND>
 >;
 
+type HasSubObjectObjectValue<A, B> = Partial<{
+  [Key in keyof A & keyof B]: A[Key] & B[Key] extends never
+    ? B[Key]
+    : A[Key] | B[Key] extends object
+      ? HasSubObjectObjectValue<A[Key], B[Key]>
+      : A[Key] & B[Key] extends object
+        ? B[Key]
+        : A[Key];
+}> & {
+  [Key in
+    | Exclude<keyof A, keyof B>
+    | Exclude<keyof B, keyof A>]: Key extends keyof B ? B[Key] : never;
+};
+
+type HasSubObjectData<
+  Data,
+  SubObject,
+  RData = Required<Data>,
+  RSubObject = Required<SubObject>,
+> = Partial<{
+  [Key in keyof RData & keyof RSubObject]: RData[Key] &
+    RSubObject[Key] extends never
+    ? RSubObject[Key]
+    : RData[Key] | RSubObject[Key] extends object
+      ? HasSubObjectObjectValue<RData[Key], RSubObject[Key]>
+      : RData[Key] & RSubObject[Key] extends object
+        ? RSubObject[Key]
+        : RData[Key];
+}> & {
+  [Key in Exclude<keyof SubObject, keyof Data>]: SubObject[Key];
+};
+
+type HasSubObjectSubObject<
+  SubObject,
+  Data,
+  RSubObject = Required<SubObject>,
+  RData = Required<Data>,
+> = Partial<{
+  [Key in keyof RData & keyof RSubObject]: RData[Key] &
+    RSubObject[Key] extends never
+    ? RData[Key]
+    : RData[Key] | RSubObject[Key] extends object
+      ? HasSubObjectObjectValue<RSubObject[Key], RData[Key]>
+      : RData[Key] & RSubObject[Key] extends object
+        ? RData[Key]
+        : RSubObject[Key];
+}> & {
+  [Key in Exclude<keyof SubObject, keyof Data>]: never;
+};
+
 /**
  * Checks if `subObject` is a sub-object of `object`, which means for every
  * property and value in `subObject`, there's the same property in `object`
@@ -25,10 +75,10 @@ type HasSubObjectGuard<T, S> = Simplify<
  * @dataFirst
  * @category Guard
  */
-export function hasSubObject<T, S extends Partial<T>>(
-  data: T,
-  subObject: S,
-): data is HasSubObjectGuard<T, S>;
+export function hasSubObject<
+  T extends Record<PropertyKey, unknown>,
+  S extends HasSubObjectSubObject<S, T>,
+>(data: T, subObject: S): data is HasSubObjectGuard<T, S>;
 
 /**
  * Checks if `subObject` is a sub-object of `object`, which means for every
@@ -45,32 +95,29 @@ export function hasSubObject<T, S extends Partial<T>>(
  * @dataLast
  * @category Guard
  */
-export function hasSubObject<T, S extends Partial<T>>(
+export function hasSubObject<S extends Record<PropertyKey, unknown>>(
   subObject: S,
-): (data: T) => data is HasSubObjectGuard<T, S>;
+): <T extends HasSubObjectData<T, S>>(
+  data: T,
+) => data is HasSubObjectGuard<T, S>;
 
 export function hasSubObject(...args: ReadonlyArray<unknown>): unknown {
   return purry(hasSubObjectImplementation, args);
 }
 
-function hasSubObjectImplementation<T extends object, S extends Partial<T>>(
-  data: T,
-  subObject: S,
-): data is HasSubObjectGuard<T, S> {
+function hasSubObjectImplementation<
+  T extends Record<PropertyKey, unknown>,
+  S extends Record<PropertyKey, unknown>,
+>(data: T, subObject: S): data is HasSubObjectGuard<T, S> {
   for (const [key, value] of Object.entries(subObject)) {
     if (!Object.hasOwn(data, key)) {
       return false;
     }
 
-    if (
-      !isDeepEqual(
-        value,
-        // @ts-expect-error [ts7053] - We already checked that `data` has `key
-        data[key],
-      )
-    ) {
+    if (!isDeepEqual(value, data[key])) {
       return false;
     }
   }
+
   return true;
 }
diff --git a/src/index.ts b/src/index.ts
index 330fc21..daf2993 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -113,6 +113,7 @@ export * from "./sortedIndexWith";
 export * from "./sortedLastIndex";
 export * from "./sortedLastIndexBy";
 export * from "./splice";
+export * from "./split";
 export * from "./splitAt";
 export * from "./splitWhen";
 export * from "./stringToPath";
diff --git a/src/internal/types.ts b/src/internal/types.ts
index 72ebf15..62f80d7 100644
--- a/src/internal/types.ts
+++ b/src/internal/types.ts
@@ -8,6 +8,7 @@ import {
   type KeysOfUnion,
   type Simplify,
   type Split,
+  type EmptyObject,
 } from "type-fest";
 
 declare const __brand: unique symbol;
@@ -96,15 +97,30 @@ type IsBoundedString<T> = T extends string
  *
  * @see EnumerableStringKeyedValueOf
  */
-export type EnumerableStringKeyOf<T> = `${Exclude<keyof T, symbol>}`;
+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> = {
+export type EnumerableStringKeyedValueOf<T> = ValuesOf<{
   [K in keyof T]-?: K extends symbol ? never : T[K];
-}[keyof T];
+}>;
+
+/**
+ * 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:
diff --git a/src/mapKeys.ts b/src/mapKeys.ts
index 8c3a6bc..49e9f19 100644
--- a/src/mapKeys.ts
+++ b/src/mapKeys.ts
@@ -29,7 +29,7 @@ export function mapKeys<T extends {}, S extends PropertyKey>(
     value: EnumerableStringKeyedValueOf<T>,
     data: T,
   ) => S,
-): ExactRecord<S, T[keyof T]>;
+): ExactRecord<S, EnumerableStringKeyedValueOf<T>>;
 
 /**
  * Maps keys of `object` and keeps the same values.
@@ -48,7 +48,7 @@ export function mapKeys<T extends {}, S extends PropertyKey>(
     value: EnumerableStringKeyedValueOf<T>,
     data: T,
   ) => S,
-): (data: T) => ExactRecord<S, T[keyof T]>;
+): (data: T) => ExactRecord<S, EnumerableStringKeyedValueOf<T>>;
 
 export function mapKeys(...args: ReadonlyArray<unknown>): unknown {
   return purry(mapKeysImplementation, args);
diff --git a/src/omitBy.ts b/src/omitBy.ts
index 81ecd4a..08435fa 100644
--- a/src/omitBy.ts
+++ b/src/omitBy.ts
@@ -19,15 +19,22 @@ type PickSymbolKeys<T extends object> = {
 // would be omitted from the output, so we need to assume any of them could be
 // filtered out. This means that we effectively make all (enumerable) keys
 // optional.
-type PartialEnumerableKeys<T extends object> = Simplify<
-  IfBoundedRecord<
-    T,
-    PickSymbolKeys<T> & {
-      -readonly [P in keyof T as P extends symbol ? never : P]?: Required<T>[P];
-    },
-    ReconstructedRecord<T>
-  >
->;
+type PartialEnumerableKeys<T extends object> =
+  // `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
+    ? Simplify<
+        IfBoundedRecord<
+          T,
+          PickSymbolKeys<T> & {
+            -readonly [P in keyof T as P extends symbol
+              ? never
+              : P]?: Required<T>[P];
+          },
+          ReconstructedRecord<T>
+        >
+      >
+    : never;
 
 // When the predicate is a type-guard we have more information to work with when
 // constructing the type of the output object. We can safely remove any property
diff --git a/src/pickBy.ts b/src/pickBy.ts
index bcd1171..e90d8f4 100644
--- a/src/pickBy.ts
+++ b/src/pickBy.ts
@@ -15,15 +15,17 @@ type EnumerableKey<T> = `${T extends number | string ? T : never}`;
 // When the predicate isn't a type-guard we don't know which properties would be
 // part of the output and which wouldn't so we can only safely downgrade the
 // whole object to a Partial of the input.
-type EnumeratedPartial<T> = Simplify<
-  IfBoundedRecord<
-    T,
-    {
-      -readonly [P in keyof T as EnumerableKey<P>]?: Required<T>[P];
-    },
-    ReconstructedRecord<T>
-  >
->;
+type EnumeratedPartial<T> = T extends unknown
+  ? Simplify<
+      IfBoundedRecord<
+        T,
+        {
+          -readonly [P in keyof T as EnumerableKey<P>]?: Required<T>[P];
+        },
+        ReconstructedRecord<T>
+      >
+    >
+  : never;
 
 // When the predicate is a type-guard we have more information to work with when
 // constructing the type of the output object. We can safely remove any property
diff --git a/src/pullObject.ts b/src/pullObject.ts
index e105e9f..0832d77 100644
--- a/src/pullObject.ts
+++ b/src/pullObject.ts
@@ -1,4 +1,4 @@
-import type { IterableContainer } from "./internal/types";
+import type { ExactRecord, IterableContainer } from "./internal/types";
 import { purry } from "./purry";
 
 /**
@@ -40,7 +40,7 @@ export function pullObject<
   data: T,
   keyExtractor: (item: T[number], index: number, data: T) => K,
   valueExtractor: (item: T[number], index: number, data: T) => V,
-): Partial<Record<K, V>>;
+): ExactRecord<K, V>;
 
 /**
  * Creates an object that maps the result of `valueExtractor` with a key
@@ -78,7 +78,7 @@ export function pullObject<
 >(
   keyExtractor: (item: T[number], index: number, data: T) => K,
   valueExtractor: (item: T[number], index: number, data: T) => V,
-): (data: T) => Partial<Record<K, V>>;
+): (data: T) => ExactRecord<K, V>;
 
 export function pullObject(...args: ReadonlyArray<unknown>): unknown {
   return purry(pullObjectImplementation, args);
diff --git a/src/split.ts b/src/split.ts
new file mode 100644
index 0000000..49db482
--- /dev/null
+++ b/src/split.ts
@@ -0,0 +1,114 @@
+import type {
+  ArraySlice,
+  IsFloat,
+  NonNegative,
+  Split as SplitBase,
+} from "type-fest";
+
+type Split<
+  S extends string,
+  Separator extends string,
+  N extends number | undefined = undefined,
+> = string extends S
+  ? Array<string>
+  : string extends Separator
+    ? Array<string>
+    : number extends N
+      ? Array<string>
+      : // TODO: We need a way to "floor" non-integer numbers, until then we return a lower fidelity type instead.
+        IsFloat<N> extends true
+        ? Array<string>
+        : ArraySlice<
+            // We can use the base (type-fest) split **only** if all the params
+            // are literals. For all other cases it would return the wrong type
+            // so we fallback to the built-in Array.prototype.split return type.
+            SplitBase<S, Separator>,
+            0,
+            // `undefined` and negative numbers are treated as non-limited
+            // splits, which is what we'd get when using `never` for ArraySlice.
+            N extends number ? NonNegative<N> : never
+          >;
+
+/**
+ * Takes a pattern and divides this string into an ordered list of substrings by
+ * searching for the pattern, puts these substrings into an array, and returns
+ * the array. This function mirrors the built-in [`String.prototype.split`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split)
+ * method.
+ *
+ * @param data - The string to split.
+ * @param separator - The pattern describing where each split should occur. Can
+ * be a string, or a regular expression.
+ * @param limit - A non-negative integer specifying a limit on the number of
+ * substrings to be included in the array. If provided, splits the string at
+ * each occurrence of the specified separator, but stops when limit entries have
+ * been placed in the array. Any leftover text is not included in the array at
+ * all. The array may contain fewer entries than limit if the end of the string
+ * is reached before the limit is reached. If limit is 0, [] is returned.
+ * @returns An Array of strings, split at each point where the separator occurs
+ * in the given string.
+ * @signature
+ *   R.split(data, separator, limit);
+ * @example
+ *   R.split("a,b,c", ","); //=> ["a", "b", "c"]
+ *   R.split("a,b,c", ",", 2); //=> ["a", "b"]
+ *   R.split("a1b2c3d", /\d/u); //=> ["a", "b", "c", "d"]
+ * @dataFirst
+ * @category String
+ */
+export function split(
+  data: string,
+  separator: RegExp,
+  limit?: number,
+): Array<string>;
+export function split<
+  S extends string,
+  Separator extends string,
+  N extends number | undefined = undefined,
+>(data: S, separator: Separator, limit?: N): Split<S, Separator, N>;
+
+/**
+ * Takes a pattern and divides this string into an ordered list of substrings by
+ * searching for the pattern, puts these substrings into an array, and returns
+ * the array. This function mirrors the built-in [`String.prototype.split`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/split)
+ * method.
+ *
+ * @param separator - The pattern describing where each split should occur. Can
+ * be a string, or a regular expression.
+ * @param limit - A non-negative integer specifying a limit on the number of
+ * substrings to be included in the array. If provided, splits the string at
+ * each occurrence of the specified separator, but stops when limit entries have
+ * been placed in the array. Any leftover text is not included in the array at
+ * all. The array may contain fewer entries than limit if the end of the string
+ * is reached before the limit is reached. If limit is 0, [] is returned.
+ * @returns An Array of strings, split at each point where the separator occurs
+ * in the given string.
+ * @signature
+ *   R.split(separator, limit)(data);
+ * @example
+ *   R.pipe("a,b,c", R.split(",")); //=> ["a", "b", "c"]
+ *   R.pipe("a,b,c", R.split(",", 2)); //=> ["a", "b"]
+ *   R.pipe("a1b2c3d", R.split(/\d/u)); //=> ["a", "b", "c", "d"]
+ * @dataLast
+ * @category String
+ */
+export function split(
+  separator: RegExp,
+  limit?: number,
+): (data: string) => Array<string>;
+export function split<
+  S extends string,
+  Separator extends string,
+  N extends number | undefined = undefined,
+>(separator: Separator, limit?: N): (data: S) => Split<S, Separator, N>;
+
+export function split(
+  dataOrSeparator: RegExp | string,
+  separatorOrLimit?: RegExp | number | string,
+  limit?: number,
+): unknown {
+  return typeof separatorOrLimit === "number" || separatorOrLimit === undefined
+    ? // dataLast
+      (data: string) => data.split(dataOrSeparator, separatorOrLimit)
+    : // dataFirst
+      (dataOrSeparator as string).split(separatorOrLimit, limit);
+}


Test patch:
diff --git a/src/forEachObj.test.ts b/src/forEachObj.test.ts
index de02e55..3f80fe6 100644
--- a/src/forEachObj.test.ts
+++ b/src/forEachObj.test.ts
@@ -70,4 +70,21 @@ describe("typing", () => {
       expectTypeOf(value).toEqualTypeOf<boolean | string>();
     });
   });
+
+  test("union of records", () => {
+    const data = {} as Record<number, string> | Record<string, number>;
+
+    forEachObj(data, (value, key) => {
+      expectTypeOf(key).toEqualTypeOf<string>();
+      expectTypeOf(value).toEqualTypeOf<number | string>();
+    });
+
+    pipe(
+      data,
+      forEachObj((value, key) => {
+        expectTypeOf(key).toEqualTypeOf<string>();
+        expectTypeOf(value).toEqualTypeOf<number | string>();
+      }),
+    );
+  });
 });
diff --git a/src/fromKeys.test.ts b/src/fromKeys.test.ts
index 140b608..9c54dc0 100644
--- a/src/fromKeys.test.ts
+++ b/src/fromKeys.test.ts
@@ -1,5 +1,6 @@
 import type { Simplify } from "type-fest";
 import { add } from "./add";
+import { constant } from "./constant";
 import { fromKeys } from "./fromKeys";
 import { pipe } from "./pipe";
 
@@ -71,74 +72,74 @@ describe("runtime", () => {
       [symbol]: "symbol",
     });
   });
-});
 
-describe("dataLast", () => {
-  it("works on trivially empty arrays", () => {
-    expect(
-      pipe(
-        [] as Array<string>,
-        fromKeys((item) => `${item}_`),
-      ),
-    ).toEqual({});
-  });
+  describe("dataLast", () => {
+    it("works on trivially empty arrays", () => {
+      expect(
+        pipe(
+          [] as Array<string>,
+          fromKeys((item) => `${item}_`),
+        ),
+      ).toEqual({});
+    });
 
-  it("works on regular arrays", () => {
-    expect(
-      pipe(
-        ["a"],
-        fromKeys((item) => `${item}_`),
-      ),
-    ).toEqual({ a: "a_" });
-  });
+    it("works on regular arrays", () => {
+      expect(
+        pipe(
+          ["a"],
+          fromKeys((item) => `${item}_`),
+        ),
+      ).toEqual({ a: "a_" });
+    });
 
-  it("works with duplicates", () => {
-    expect(
-      pipe(
-        ["a", "a"],
-        fromKeys((item) => `${item}_`),
-      ),
-    ).toEqual({ a: "a_" });
-  });
+    it("works with duplicates", () => {
+      expect(
+        pipe(
+          ["a", "a"],
+          fromKeys((item) => `${item}_`),
+        ),
+      ).toEqual({ a: "a_" });
+    });
 
-  it("uses the last value", () => {
-    let counter = 0;
-    expect(
-      pipe(
-        ["a", "a"],
-        fromKeys(() => {
-          counter += 1;
-          return counter;
-        }),
-      ),
-    ).toEqual({ a: 2 });
-  });
+    it("uses the last value", () => {
+      let counter = 0;
+      expect(
+        pipe(
+          ["a", "a"],
+          fromKeys(() => {
+            counter += 1;
+            return counter;
+          }),
+        ),
+      ).toEqual({ a: 2 });
+    });
 
-  it("works with number keys", () => {
-    expect(pipe([123], fromKeys(add(1)))).toEqual({ 123: 124 });
-  });
+    it("works with number keys", () => {
+      expect(pipe([123], fromKeys(add(1)))).toEqual({ 123: 124 });
+    });
 
-  it("works with symbols", () => {
-    const symbol = Symbol("a");
-    expect(
-      pipe(
-        [symbol],
-        fromKeys(() => 1),
-      ),
-    ).toEqual({ [symbol]: 1 });
-  });
+    it("works with symbols", () => {
+      const symbol = Symbol("a");
+      expect(
+        pipe(
+          [symbol],
+          fromKeys(() => 1),
+        ),
+      ).toEqual({ [symbol]: 1 });
+    });
 
-  it("works with a mix of key types", () => {
-    const symbol = Symbol("a");
-    expect(
-      pipe(
-        ["a", 123, symbol],
-        fromKeys((item) => typeof item),
-      ),
-    ).toEqual({
-      a: "string",
-      123: "number",
-      [symbol]: "symbol",
+    it("works with a mix of key types", () => {
+      const symbol = Symbol("a");
+      expect(
+        pipe(
+          ["a", 123, symbol],
+          fromKeys((item) => typeof item),
+        ),
+      ).toEqual({
+        a: "string",
+        123: "number",
+        [symbol]: "symbol",
+      });
     });
   });
 });
@@ -147,11 +148,11 @@ describe("typing", () => {
   test("empty array", () => {
     const data = [] as const;
 
-    const dataFirst = fromKeys(data, getVal);
+    const dataFirst = fromKeys(data, constant(1));
     // eslint-disable-next-line @typescript-eslint/ban-types -- That's just what we return
     expectTypeOf(dataFirst).toEqualTypeOf<{}>();
 
-    const dataLast = pipe(data, fromKeys(getVal));
+    const dataLast = pipe(data, fromKeys(constant(1)));
     // eslint-disable-next-line @typescript-eslint/ban-types -- That's just what we return
     expectTypeOf(dataLast).toEqualTypeOf<{}>();
   });
@@ -159,10 +160,10 @@ describe("typing", () => {
   test("fixed tuple", () => {
     const data = ["cat", "dog"] as const;
 
-    const dataFirst = fromKeys(data, getVal);
+    const dataFirst = fromKeys(data, constant(1));
     expectTypeOf(dataFirst).toEqualTypeOf<Record<"cat" | "dog", number>>();
 
-    const dataLast = pipe(data, fromKeys(getVal));
+    const dataLast = pipe(data, fromKeys(constant(1)));
     expectTypeOf(dataLast).toEqualTypeOf<Record<"cat" | "dog", number>>();
   });
 
@@ -170,30 +171,30 @@ describe("typing", () => {
     test("regular array", () => {
       const data = [] as Array<string>;
 
-      const dataFirst = fromKeys(data, getVal);
-      expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<string, number>>>();
+      const dataFirst = fromKeys(data, constant(1));
+      expectTypeOf(dataFirst).toEqualTypeOf<Record<string, number>>();
 
-      const dataLast = pipe(data, fromKeys(getVal));
-      expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<string, number>>>();
+      const dataLast = pipe(data, fromKeys(constant(1)));
+      expectTypeOf(dataLast).toEqualTypeOf<Record<string, number>>();
     });
 
     test("non-empty array", () => {
       const data = ["cat"] as [string, ...Array<string>];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<Record<string, number>>();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<Record<string, number>>();
     });
 
     test("fixed tuple", () => {
       const data = ["cat", "dog"] as [string, string];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<Record<string, number>>();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<Record<string, number>>();
     });
   });
@@ -202,12 +203,12 @@ describe("typing", () => {
     test("regular array", () => {
       const data = [] as Array<"cat" | "dog">;
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Partial<Record<"cat" | "dog", number>>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Partial<Record<"cat" | "dog", number>>
       >();
@@ -216,7 +217,7 @@ describe("typing", () => {
     test("non-empty array", () => {
       const data = ["cat"] as ["cat" | "dog", ...Array<"mouse" | "pig">];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Simplify<
           Partial<Record<"mouse" | "pig", number>> &
@@ -224,7 +225,7 @@ describe("typing", () => {
         >
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Simplify<
           Partial<Record<"mouse" | "pig", number>> &
@@ -236,7 +237,7 @@ describe("typing", () => {
     test("fixed tuple", () => {
       const data = ["cat", "mouse"] as ["cat" | "dog", "mouse" | "pig"];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Simplify<
           ({ cat: number } | { dog: number }) &
@@ -244,7 +245,7 @@ describe("typing", () => {
         >
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Simplify<
           ({ cat: number } | { dog: number }) &
@@ -258,14 +259,14 @@ describe("typing", () => {
     test("regular array", () => {
       const data = [] as Array<`prefix_${number}`>;
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
-        Partial<Record<`prefix_${number}`, number>>
+        Record<`prefix_${number}`, number>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
-        Partial<Record<`prefix_${number}`, number>>
+        Record<`prefix_${number}`, number>
       >();
     });
 
@@ -275,12 +276,12 @@ describe("typing", () => {
         ...Array<`prefix_${number}`>,
       ];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Record<`prefix_${number}`, number>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Record<`prefix_${number}`, number>
       >();
@@ -292,12 +293,12 @@ describe("typing", () => {
         `${number}_suffix`,
       ];
 
-      const dataFirst = fromKeys(data, getVal);
+      const dataFirst = fromKeys(data, constant(1));
       expectTypeOf(dataFirst).toEqualTypeOf<
         Record<`${number}_suffix` | `prefix_${number}`, number>
       >();
 
-      const dataLast = pipe(data, fromKeys(getVal));
+      const dataLast = pipe(data, fromKeys(constant(1)));
       expectTypeOf(dataLast).toEqualTypeOf<
         Record<`${number}_suffix` | `prefix_${number}`, number>
       >();
@@ -307,42 +308,40 @@ describe("typing", () => {
       test("regular array", () => {
         const data = [] as Array<number>;
 
-        const dataFirst = fromKeys(data, getVal);
-        expectTypeOf(dataFirst).toEqualTypeOf<
-          Partial<Record<number, number>>
-        >();
+        const dataFirst = fromKeys(data, constant(1));
+        expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
-        expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<number, number>>>();
+        const dataLast = pipe(data, fromKeys(constant(1)));
+        expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
       });
 
       test("non-empty array", () => {
         const data = [1] as [number, ...Array<number>];
 
-        const dataFirst = fromKeys(data, getVal);
+        const dataFirst = fromKeys(data, constant(1));
         expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
+        const dataLast = pipe(data, fromKeys(constant(1)));
         expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
       });
 
       test("fixed tuple", () => {
         const data = [1, 2] as [number, number];
 
-        const dataFirst = fromKeys(data, getVal);
+        const dataFirst = fromKeys(data, constant(1));
         expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
+        const dataLast = pipe(data, fromKeys(constant(1)));
         expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
       });
 
       test("literals", () => {
         const data = [1, 2, 3] as const;
 
-        const dataFirst = fromKeys(data, getVal);
+        const dataFirst = fromKeys(data, constant(1));
         expectTypeOf(dataFirst).toEqualTypeOf<Record<1 | 2 | 3, number>>();
 
-        const dataLast = pipe(data, fromKeys(getVal));
+        const dataLast = pipe(data, fromKeys(constant(1)));
         expectTypeOf(dataLast).toEqualTypeOf<Record<1 | 2 | 3, number>>();
       });
     });
@@ -351,18 +350,14 @@ describe("typing", () => {
   test("typescript doesn't choke on huge literal unions", () => {
     const data = [] as Array<`${Letter}${Letter}`>;
 
-    const dataFirst = fromKeys(data, getVal);
+    const dataFirst = fromKeys(data, constant(1));
     expectTypeOf(dataFirst).toEqualTypeOf<
       Partial<Record<`${Letter}${Letter}`, number>>
     >();
 
-    const dataLast = pipe(data, fromKeys(getVal));
+    const dataLast = pipe(data, fromKeys(constant(1)));
     expectTypeOf(dataLast).toEqualTypeOf<
       Partial<Record<`${Letter}${Letter}`, number>>
     >();
   });
 });
-
-function getVal(): number {
-  return 1;
-}
diff --git a/src/hasSubObject.test.ts b/src/hasSubObject.test.ts
index 764da81..54f20eb 100644
--- a/src/hasSubObject.test.ts
+++ b/src/hasSubObject.test.ts
@@ -59,37 +59,122 @@ describe("data last", () => {
 
 describe("typing", () => {
   describe("data-first", () => {
-    it("must have matching keys and values", () => {
+    it("returns boolean", () => {
       expectTypeOf(hasSubObject({ a: 2 }, { a: 1 })).toEqualTypeOf<boolean>();
+    });
+
+    it("doesn't require sub-object to have all keys", () => {
+      // ok
+      hasSubObject({ a: 1, b: 2 }, { b: 2 });
+    });
 
-      // @ts-expect-error [ts2353] - missing a key
+    it("doesn't allow sub-object to have distinct keys from super-object", () => {
+      // @ts-expect-error [ts2322] - non-matching key
       hasSubObject({ b: 2 }, { a: 1 });
 
-      // @ts-expect-error [ts2322] - different value type
+      // @ts-expect-error [ts2322] - non-matching key
+      hasSubObject({ b: 2 }, { b: 2, a: 1 });
+    });
+
+    it("doesn't allow keys to have different types", () => {
+      // @ts-expect-error [ts2322] - `number` isn't assignable to `string`
       hasSubObject({ a: "a" }, { a: 1 });
 
-      // ok - wider value type
-      hasSubObject({ a: "a" } as { a: number | string }, { a: 1 });
+      // @ts-expect-error [ts2322] - object isn't assignable to `string`
+      hasSubObject({ a: "a" }, { a: { b: 2 } });
+
+      // @ts-expect-error [ts2322] - `string` isn't assignable to object
+      hasSubObject({ a: { b: 2 } }, { a: "a" });
+    });
 
-      // @ts-expect-error [ts2345] - narrower value type
-      hasSubObject({ a: "a" }, { a: 1 } as { a: number | string });
+    it("allows keys to have different types when types are overlapping", () => {
+      // ok - union type in super-object
+      hasSubObject({ a: "a" as number | string }, { a: 1 });
 
-      // ok - unknown data type
-      hasSubObject({ a: 2 } as unknown, { a: 1 });
+      // ok - union type in sub-object
+      hasSubObject({ a: "a" }, { a: 1 as number | string });
 
+      // ok - union type in both super-object and sub-object
+      hasSubObject({ a: "a" as number | string }, { a: 1 as boolean | number });
+    });
+
+    it("allows const types", () => {
       // ok - const value
-      hasSubObject({ a: 2 }, { a: 1 } as const);
+      hasSubObject({ a: 2, b: 2 }, { a: 1 } as const);
+    });
+
+    it("only allows valid objects to be passed", () => {
+      // @ts-expect-error [ts2345] - only allow valid objects to be passed
+      hasSubObject({ a: 2 } as unknown, { a: 1 });
+
+      // @ts-expect-error [ts2345] - only allow valid objects to be passed
+      hasSubObject({ a: 2 }, { a: 1 } as unknown);
     });
 
     it("allows nested objects", () => {
-      // ok - nested object has extra keys
-      hasSubObject({ a: { b: 4 } }, { a: { b: 1, c: 2 } });
+      // ok - nested objects
+      hasSubObject({ a: { b: 1, c: 2 } }, { a: { b: 1, c: 2 } });
+
+      // ok - deep-nested objects
+      hasSubObject({ a: { b: { c: 2 } } }, { a: { b: { c: 2 } } });
+
+      // ok - optional keys in sub-object
+      hasSubObject({ a: { b: 1 } }, {
+        a: { b: 1 },
+      } as { a?: { b?: number } });
 
-      // @ts-expect-error [ts2741] - nested object has missing keys
+      // ok - optional keys in super-object
+      hasSubObject({ a: { b: 1 } } as { a?: { b?: number } }, {
+        a: { b: 1 },
+      });
+    });
+
+    it("doesn't allow nested objects to have missing keys", () => {
+      // @ts-expect-error [ts2322] - nested sub-object doesn't have `c` key
       hasSubObject({ a: { b: 1, c: 2 } }, { a: { b: 1 } });
 
-      // @ts-expect-error [ts2322] - nested object has wrong value types
+      // @ts-expect-error [ts2379] - nested sub-object doesn't have `c` key (works with optional key)
+      hasSubObject({ a: { b: 1, c: 2 } }, { a: { b: 1 } } as {
+        a?: { b: number };
+      });
+    });
+
+    it("doesn't allow nested objects to have extra keys", () => {
+      // @ts-expect-error [ts2322] - nested sub-object has extra key `c`
+      hasSubObject({ a: { b: 1 } }, { a: { b: 1, c: 2 } });
+
+      hasSubObject({ a: { b: 1 } } as { a?: { b: number } }, {
+        // @ts-expect-error ts[2322] - nested sub-object has extra key `c` (works with optional key)
+        a: { b: 1, c: 2 },
+      });
+    });
+
+    it("doesn't allow nested objects keys to have different types", () => {
+      // @ts-expect-error [ts2322] - nested sub-object has wrong value types
       hasSubObject({ a: { b: 4, c: "c" } }, { a: { b: 1, c: 2 } });
+
+      // @ts-expect-error [ts2322] - deep-nested sub-object has wrong value types
+      hasSubObject({ a: { b: { c: 2 } } }, { a: { b: { c: "2" } } });
+
+      // @ts-expect-error [ts2322] - properly compares primitive and non-primitive types
+      hasSubObject({ a: { b: "a" } }, { a: { b: { c: 2 } } });
+
+      // @ts-expect-error [ts2322] - properly compares primitive and non-primitive types
+      hasSubObject({ a: { b: { c: 2 } } }, { a: { b: "a" } });
+    });
+
+    it("allows nested objects keys to have different types when types are overlapping", () => {
+      // ok - union type in nested super-object
+      hasSubObject({ a: { b: 1 as number | string } }, { a: { b: 1 } });
+
+      // ok - union type in nested sub-object
+      hasSubObject({ a: { b: 1 } }, { a: { b: 1 as number | string } });
+
+      // ok - union type in both nested super-object and sub-object
+      hasSubObject(
+        { a: { b: 1 as boolean | number } },
+        { a: { b: 1 as number | string } },
+      );
     });
 
     it("narrows with empty object", () => {
@@ -126,47 +211,395 @@ describe("typing", () => {
       const obj = {} as { a?: string; b?: number };
 
       if (hasSubObject(obj, { a: "a" } as const)) {
-        expectTypeOf(obj).toMatchTypeOf<{ readonly a: "a"; b?: number }>();
+        expectTypeOf(obj).toMatchTypeOf<{ a: "a"; b?: number }>();
       } else {
         expectTypeOf(obj).toMatchTypeOf<{ a?: string; b?: number }>();
       }
     });
+
+    it("narrows with sub-object union type field", () => {
+      const obj: { a?: string; b?: number; c?: boolean } = {};
+
+      if (hasSubObject(obj, { c: true as boolean | number })) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c: boolean;
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c?: boolean;
+        }>();
+      }
+    });
+
+    it("narrows union types", () => {
+      const obj: { a?: string; b?: number; c?: number | string } = {};
+
+      if (hasSubObject(obj, { c: true } as { c?: boolean | number })) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c?: number;
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c?: number | string;
+        }>();
+      }
+    });
+
+    it("narrows nested fields with sub-object union types", () => {
+      const obj = {
+        a: { foo: "test", bar: true },
+        b: { foo: "test", bar: true },
+      };
+
+      if (
+        hasSubObject(obj, {
+          a: { foo: 12 as number | string, bar: true },
+          b: { foo: "test", bar: true as boolean | number },
+        })
+      ) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: string; bar: boolean };
+          b: { foo: string; bar: boolean };
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: string; bar: boolean };
+          b: { foo: string; bar: boolean };
+        }>();
+      }
+    });
+
+    it("narrows nested fields with union types", () => {
+      const obj = {
+        a: { foo: "test" as number | string, bar: true },
+        b: { foo: "test", bar: true as boolean | number },
+      };
+
+      if (
+        hasSubObject(obj, {
+          a: { foo: 12, bar: true },
+          b: { foo: "test", bar: true },
+        })
+      ) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: number; bar: boolean };
+          b: { foo: string; bar: boolean };
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: number | string; bar: boolean };
+          b: { foo: string; bar: boolean | number };
+        }>();
+      }
+    });
   });
 
   describe("data-last", () => {
-    it("must have matching keys and values", () => {
+    it("returns boolean", () => {
       expectTypeOf(
         pipe({ a: 2 }, hasSubObject({ a: 1 })),
       ).toEqualTypeOf<boolean>();
+    });
+
+    it("doesn't require sub-object to have all keys", () => {
+      // ok
+      hasSubObject({ b: 2 })({ a: 1, b: 2 });
+      // ok
+      pipe({ a: 1, b: 2 }, hasSubObject({ b: 2 }));
+    });
 
-      // @ts-expect-error [ts2353] - missing a key
+    it("doesn't allow sub-object to have distinct keys from super-object", () => {
+      // @ts-expect-error [ts2353] - non-matching key
+      hasSubObject({ a: 1 })({ b: 2 });
+      // @ts-expect-error [ts2345] - non-matching key
       pipe({ b: 2 }, hasSubObject({ a: 1 }));
 
-      // @ts-expect-error [ts2345] - different value type
+      // @ts-expect-error [ts2353] - non-matching key
+      hasSubObject({ b: 2, a: 1 })({ b: 2 });
+      // @ts-expect-error [ts2345] - non-matching key
+      pipe({ b: 2 }, hasSubObject({ b: 2, a: 1 }));
+    });
+
+    it("doesn't allow keys to have different types", () => {
+      // @ts-expect-error [ts2322] - `string` isn't assignable to `number`
+      hasSubObject({ a: 1 })({ a: "a" });
+      // @ts-expect-error [ts2345] - `string` isn't assignable to `number`
       pipe({ a: "a" }, hasSubObject({ a: 1 }));
 
-      // ok - wider value type
-      pipe({ a: "a" } as { a: number | string }, hasSubObject({ a: 1 }));
+      // @ts-expect-error [ts2322] - `string` isn't assignable to object
+      hasSubObject({ a: { b: 2 } })({ a: "a" });
+      // @ts-expect-error [ts2345] - `string` isn't assignable to object
+      pipe({ a: "a" }, hasSubObject({ a: { b: 2 } }));
 
-      // @ts-expect-error [ts2345] - narrower value type
-      pipe({ a: "a" }, hasSubObject({ a: 1 } as { a: number | string }));
+      // @ts-expect-error [ts2322] - object isn't assignable to `string`
+      hasSubObject({ a: "a" })({ a: { b: 2 } });
+      // @ts-expect-error [ts2345] - object isn't assignable to `string`
+      pipe({ a: { b: 2 } }, hasSubObject({ a: "a" }));
+    });
 
-      // ok - unknown data type
-      pipe({ a: 2 } as unknown, hasSubObject({ a: 1 }));
+    it("allows keys to have different types when types are overlapping", () => {
+      // ok - union type in super-object
+      hasSubObject({ a: 1 })({ a: "a" as number | string });
+      pipe({ a: "a" as number | string }, hasSubObject({ a: 1 }));
+
+      // ok - union type in sub-object
+      hasSubObject({ a: 1 as number | string })({ a: "a" });
+      pipe({ a: "a" }, hasSubObject({ a: 1 as number | string }));
+
+      // ok - union type in both super-object and sub-object
+      hasSubObject({ a: 1 as boolean | number })({
+        a: "a" as number | string,
+      });
+      pipe(
+        { a: "a" as number | string },
+        hasSubObject({ a: 1 as boolean | number }),
+      );
+    });
 
+    it("allows const types", () => {
       // ok - const value
-      pipe({ a: 2 }, hasSubObject({ a: 1 } as const));
+      hasSubObject({ a: 1 } as const)({ a: 2, b: 2 });
+      pipe({ a: 2, b: 2 }, hasSubObject({ a: 1 } as const));
+    });
+
+    it("only allows valid objects to be passed", () => {
+      // @ts-expect-error [ts2345] - only allow valid objects to be passed
+      hasSubObject({ a: 1 })({ a: 2 } as unknown);
+      // @ts-expect-error [ts2345] - only allow valid objects to be passed
+      pipe({ a: 2 } as unknown, hasSubObject({ a: 1 }));
     });
 
     it("allows nested objects", () => {
-      // ok - nested object has extra keys
-      pipe({ a: { b: 4 } }, hasSubObject({ a: { b: 1, c: 2 } }));
+      // ok - nested objects
+      hasSubObject({ a: { b: 1, c: 2 } })({ a: { b: 1, c: 2 } });
+      pipe({ a: { b: 1, c: 2 } }, hasSubObject({ a: { b: 1, c: 2 } }));
+
+      // ok - deep-nested sub-object
+      hasSubObject({ a: { b: { c: 2 } } })({ a: { b: { c: 2 } } });
+      pipe({ a: { b: { c: 2 } } }, hasSubObject({ a: { b: { c: 2 } } }));
+
+      // ok - optional keys in sub-object
+      hasSubObject({ a: { b: 1 } } as { a?: { b?: number } })({ a: { b: 1 } });
+      pipe(
+        { a: { b: 1 } },
+        hasSubObject({ a: { b: 1 } } as { a?: { b?: number } }),
+      );
+
+      // ok - optional keys in super-object
+      hasSubObject({ a: { b: 1 } })({ a: { b: 1 } } as { a?: { b?: number } });
+      pipe(
+        { a: { b: 1 } } as { a?: { b?: number } },
+        hasSubObject({ a: { b: 1 } }),
+      );
+    });
 
-      // @ts-expect-error [ts2345] - nested object has missing keys
+    it("doesn't allow nested objects to have missing keys", () => {
+      // @ts-expect-error [ts2322] - nested sub-object doesn't have `c` key
+      hasSubObject({ a: { b: 1 } })({ a: { b: 1, c: 2 } });
+      // @ts-expect-error [ts2345] - nested sub-object doesn't have `c` key
       pipe({ a: { b: 1, c: 2 } }, hasSubObject({ a: { b: 1 } }));
 
-      // @ts-expect-error [ts2345] - nested object has wrong value types
+      hasSubObject({ a: { b: 1 } } as { a?: { b: number } })({
+        // @ts-expect-error [ts2322] - nested sub-object doesn't have `c` key (works with optional key)
+        a: { b: 1, c: 2 },
+      });
+      pipe(
+        { a: { b: 1, c: 2 } },
+        // @ts-expect-error [ts2345] - nested sub-object doesn't have `c` key (works with optional key)
+        hasSubObject({ a: { b: 1 } } as { a?: { b: number } }),
+      );
+    });
+
+    it("doesn't allow nested objects to have extra keys", () => {
+      // @ts-expect-error [ts2322] - nested sub-object has extra key `c`
+      hasSubObject({ a: { b: 1, c: 2 } })({ a: { b: 1 } });
+      // @ts-expect-error [ts2345] - nested sub-object has extra key `c`
+      pipe({ a: { b: 1 } }, hasSubObject({ a: { b: 1, c: 2 } }));
+
+      // @ts-expect-error [ts2379] - nested sub-object has extra key `c` (works with optional key)
+      hasSubObject({ a: { b: 1, c: 2 } })({ a: { b: 1 } } as {
+        a?: { b: number };
+      });
+      pipe(
+        { a: { b: 1 } } as { a?: { b: number } },
+        // @ts-expect-error [ts2345] - nested sub-object has extra key `c` (works with optional key)
+        hasSubObject({ a: { b: 1, c: 2 } }),
+      );
+    });
+
+    it("doesn't allow nested objects keys to have different types", () => {
+      // @ts-expect-error [ts2322] - nested sub-object has wrong value types
+      hasSubObject({ a: { b: 1, c: 2 } })({ a: { b: 4, c: "c" } });
+      // @ts-expect-error [ts2345] - nested sub-object has wrong value types
       pipe({ a: { b: 4, c: "c" } }, hasSubObject({ a: { b: 1, c: 2 } }));
+
+      // @ts-expect-error [ts2322] - deep-nested sub-object has wrong value types
+      hasSubObject({ a: { b: { c: "2" } } })({ a: { b: { c: 2 } } });
+      // @ts-expect-error [ts2345] - deep-nested sub-object has wrong value types
+      pipe({ a: { b: { c: 2 } } }, hasSubObject({ a: { b: { c: "2" } } }));
+
+      // @ts-expect-error [ts2322] - properly compares primitive and non-primitive types
+      hasSubObject({ a: { b: { c: 2 } } })({ a: { b: "a" } });
+      // @ts-expect-error [ts2345] - properly compares primitive and non-primitive types
+      pipe({ a: { b: "a" } }, hasSubObject({ a: { b: { c: 2 } } }));
+
+      // @ts-expect-error [ts2322] - properly compares primitive and non-primitive types
+      hasSubObject({ a: { b: "a" } })({ a: { b: { c: 2 } } });
+      // @ts-expect-error [ts2345] - properly compares primitive and non-primitive types
+      pipe({ a: { b: { c: 2 } } }, hasSubObject({ a: { b: "a" } }));
+    });
+
+    it("allows nested objects keys to have different types when types are overlapping", () => {
+      // ok - union type in nested super-object
+      hasSubObject({ a: { b: 1 } })({ a: { b: 1 as number | string } });
+      pipe({ a: { b: 1 as number | string } }, hasSubObject({ a: { b: 1 } }));
+
+      // ok - union type in nested sub-object
+      hasSubObject({ a: { b: 1 as number | string } })({ a: { b: 1 } });
+      pipe({ a: { b: 1 } }, hasSubObject({ a: { b: 1 as number | string } }));
+
+      // ok - union type in both nested super-object and sub-object
+      hasSubObject({
+        a: { b: 1 as number | string },
+      })({
+        a: { b: 1 as boolean | number },
+      });
+      pipe(
+        { a: { b: 1 as boolean | number } },
+        hasSubObject({ a: { b: 1 as number | string } }),
+      );
+    });
+
+    it("narrows with empty object", () => {
+      const obj = {} as { a?: string; b?: number };
+
+      if (hasSubObject({})(obj)) {
+        expectTypeOf(obj).toMatchTypeOf<{ a?: string; b?: number }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{ a?: string; b?: number }>();
+      }
+    });
+
+    it("narrows with same object", () => {
+      const obj = {} as { a?: string; b?: number };
+
+      if (hasSubObject(obj)(obj)) {
+        expectTypeOf(obj).toMatchTypeOf<{ a?: string; b?: number }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{ a?: string; b?: number }>();
+      }
+    });
+
+    it("narrows optional field to required", () => {
+      const obj = {} as { a?: string; b?: number };
+
+      if (hasSubObject({ a: "a" })(obj)) {
+        expectTypeOf(obj).toMatchTypeOf<{ a: string; b?: number }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{ a?: string; b?: number }>();
+      }
+    });
+
+    it("narrows field to constant type", () => {
+      const obj = {} as { a?: string; b?: number };
+
+      if (hasSubObject({ a: "a" } as const)(obj)) {
+        expectTypeOf(obj).toMatchTypeOf<{ a: "a"; b?: number }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{ a?: string; b?: number }>();
+      }
+    });
+
+    it("narrows with sub-object union type field", () => {
+      const obj: { a?: string; b?: number; c?: boolean } = {};
+
+      if (hasSubObject({ c: true as boolean | number })(obj)) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c: boolean;
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c?: boolean;
+        }>();
+      }
+    });
+
+    it("narrows union types", () => {
+      const obj: { a?: string; b?: number; c?: number | string } = {};
+
+      if (hasSubObject({ c: true } as { c?: boolean | number })(obj)) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c?: number;
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a?: string;
+          b?: number;
+          c?: number | string;
+        }>();
+      }
+    });
+
+    it("narrows nested fields with sub-object union types", () => {
+      const obj = {
+        a: { foo: "test", bar: true },
+        b: { foo: "test", bar: true },
+      };
+
+      if (
+        hasSubObject({
+          a: { foo: 12 as number | string, bar: true },
+          b: { foo: "test", bar: true as boolean | number },
+        })(obj)
+      ) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: string; bar: boolean };
+          b: { foo: string; bar: boolean };
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: string; bar: boolean };
+          b: { foo: string; bar: boolean };
+        }>();
+      }
+    });
+
+    it("narrows nested fields with union types", () => {
+      const obj = {
+        a: { foo: "test" as number | string, bar: true },
+        b: { foo: "test", bar: true as boolean | number },
+      };
+
+      if (
+        hasSubObject({
+          a: { foo: 12, bar: true },
+          b: { foo: "test", bar: true },
+        })(obj)
+      ) {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: number; bar: boolean };
+          b: { foo: string; bar: boolean };
+        }>();
+      } else {
+        expectTypeOf(obj).toMatchTypeOf<{
+          a: { foo: number | string; bar: boolean };
+          b: { foo: string; bar: boolean | number };
+        }>();
+      }
     });
   });
 });
diff --git a/src/internal/types.test.ts b/src/internal/types.test.ts
index 8b928b2..3f4cf77 100644
--- a/src/internal/types.test.ts
+++ b/src/internal/types.test.ts
@@ -1,4 +1,10 @@
-import { type Branded, type IfBoundedRecord } from "./types";
+import { type EmptyObject } from "type-fest";
+import {
+  type Branded,
+  type IfBoundedRecord,
+  type EnumerableStringKeyedValueOf,
+  type EnumerableStringKeyOf,
+} from "./types";
 
 declare const SymbolFoo: unique symbol;
 declare const SymbolBar: unique symbol;
@@ -156,3 +162,147 @@ describe("IfBoundedRecord", () => {
     >().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<Branded<string, symbol>, unknown>>
+    >().toEqualTypeOf<`${Branded<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>();
+  });
+});
diff --git a/src/keys.test.ts b/src/keys.test.ts
index 2c50c3c..db140a1 100644
--- a/src/keys.test.ts
+++ b/src/keys.test.ts
@@ -1,4 +1,5 @@
 import { keys } from "./keys";
+import { pipe } from "./pipe";
 
 describe("runtime", () => {
   describe("dataFirst", () => {
@@ -111,6 +112,20 @@ describe("typing", () => {
       expectTypeOf(result).toEqualTypeOf<[]>();
     });
 
+    test("unions of records", () => {
+      const data = {} as Record<number, unknown> | Record<string, unknown>;
+
+      const dataFirst = keys(data);
+      expectTypeOf(dataFirst).toEqualTypeOf<
+        Array<`${number}`> | Array<string>
+      >();
+
+      const dataLast = pipe(data, keys());
+      expectTypeOf(dataLast).toEqualTypeOf<
+        Array<`${number}`> | Array<string>
+      >();
+    });
+
     test("simple (required) object", () => {
       const result = keys({ a: "a", b: 1, c: true } as {
         a: string;
diff --git a/src/mapKeys.test.ts b/src/mapKeys.test.ts
index cc02170..21f7514 100644
--- a/src/mapKeys.test.ts
+++ b/src/mapKeys.test.ts
@@ -105,4 +105,14 @@ describe("typing", () => {
     const result = mapKeys({ a: "b" }, constant(123));
     expectTypeOf(result).toEqualTypeOf<Partial<Record<123, string>>>();
   });
+
+  test("union of records", () => {
+    const data = {} as Record<PropertyKey, "cat"> | Record<PropertyKey, "dog">;
+
+    const dataFirst = mapKeys(data, constant("hello" as string));
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<string, "cat" | "dog">>();
+
+    const dataLast = pipe(data, mapKeys(constant("hello" as string)));
+    expectTypeOf(dataLast).toEqualTypeOf<Record<string, "cat" | "dog">>();
+  });
 });
diff --git a/src/mapValues.test.ts b/src/mapValues.test.ts
index 478d415..4aa9c28 100644
--- a/src/mapValues.test.ts
+++ b/src/mapValues.test.ts
@@ -1,4 +1,5 @@
 import { constant } from "./constant";
+import { type Branded } from "./internal/types";
 import { mapValues } from "./mapValues";
 import { pipe } from "./pipe";
 
@@ -106,8 +107,7 @@ describe("typing", () => {
 
   describe("branded types", () => {
     test("should infer types correctly in the mapper", () => {
-      type Branded<K, T> = K & { _type: T };
-      type UserID = Branded<string, "UserId">;
+      type UserID = Branded<string, symbol>;
 
       const userValues: Record<UserID, number> = {
         ["U1" as UserID]: 1,
@@ -160,4 +160,18 @@ describe("typing", () => {
       d: boolean;
     }>();
   });
+
+  test("unions of records", () => {
+    const data = {} as Record<number, unknown> | Record<string, unknown>;
+
+    const dataFirst = mapValues(data, constant("hello" as string));
+    expectTypeOf(dataFirst).toEqualTypeOf<
+      Record<`${number}`, string> | Record<string, string>
+    >();
+
+    const dataLast = pipe(data, mapValues(constant("hello" as string)));
+    expectTypeOf(dataLast).toEqualTypeOf<
+      Record<`${number}`, string> | Record<string, string>
+    >();
+  });
 });
diff --git a/src/omitBy.test.ts b/src/omitBy.test.ts
index 246bfae..b251b59 100644
--- a/src/omitBy.test.ts
+++ b/src/omitBy.test.ts
@@ -207,6 +207,20 @@ describe("typing", () => {
       const result = omitBy(data, constant(true));
       expectTypeOf(result).toEqualTypeOf<Record<string, string>>();
     });
+
+    test("union of records", () => {
+      const data = {} as Record<number, string> | Record<string, string>;
+
+      const dataFirst = omitBy(data, constant(true));
+      expectTypeOf(dataFirst).toEqualTypeOf<
+        Record<`${number}`, string> | Record<string, string>
+      >();
+
+      const dataLast = pipe(data, omitBy(constant(true)));
+      expectTypeOf(dataLast).toEqualTypeOf<
+        Record<`${number}`, string> | Record<string, string>
+      >();
+    });
   });
 });
 
diff --git a/src/pickBy.test.ts b/src/pickBy.test.ts
index acde289..13af536 100644
--- a/src/pickBy.test.ts
+++ b/src/pickBy.test.ts
@@ -182,5 +182,18 @@ describe("typing", () => {
       const result = pickBy(data, constant(true));
       expectTypeOf(result).toEqualTypeOf<Record<string, string>>();
     });
+
+    test("union of records", () => {
+      const data = {} as Record<number, string> | Record<string, string>;
+      const dataFirst = pickBy(data, constant(true));
+      expectTypeOf(dataFirst).toEqualTypeOf<
+        Record<`${number}`, string> | Record<string, string>
+      >();
+
+      const dataLast = pipe(data, pickBy(constant(true)));
+      expectTypeOf(dataLast).toEqualTypeOf<
+        Record<`${number}`, string> | Record<string, string>
+      >();
+    });
   });
 });
diff --git a/src/pullObject.test.ts b/src/pullObject.test.ts
index 76aac03..57f96f2 100644
--- a/src/pullObject.test.ts
+++ b/src/pullObject.test.ts
@@ -174,30 +174,30 @@ describe("typing", () => {
     const data = ["a", "b"];
 
     const dataFirst = pullObject(data, identity(), constant("value"));
-    expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<string, string>>>();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<string, string>>();
 
     const dataLast = pipe(data, pullObject(identity(), constant("value")));
-    expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<string, string>>>();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<string, string>>();
   });
 
   test("number keys", () => {
     const data = [1, 2];
 
     const dataFirst = pullObject(data, identity(), constant(3));
-    expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<number, number>>>();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<number, number>>();
 
     const dataLast = pipe(data, pullObject(identity(), constant(3)));
-    expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<number, number>>>();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<number, number>>();
   });
 
   test("symbol keys", () => {
     const data = [Symbol("a"), Symbol("b")];
 
     const dataFirst = pullObject(data, identity(), constant(Symbol("c")));
-    expectTypeOf(dataFirst).toEqualTypeOf<Partial<Record<symbol, symbol>>>();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<symbol, symbol>>();
 
     const dataLast = pipe(data, pullObject(identity(), constant(Symbol("c"))));
-    expectTypeOf(dataLast).toEqualTypeOf<Partial<Record<symbol, symbol>>>();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<symbol, symbol>>();
   });
 
   test("number constants", () => {
@@ -249,16 +249,12 @@ describe("typing", () => {
       (item) => `prefix_${item}`,
       constant("value"),
     );
-    expectTypeOf(dataFirst).toEqualTypeOf<
-      Partial<Record<`prefix_${number}`, string>>
-    >();
+    expectTypeOf(dataFirst).toEqualTypeOf<Record<`prefix_${number}`, string>>();
 
     const dataLast = pipe(
       data,
       pullObject((item) => `prefix_${item}`, constant("value")),
     );
-    expectTypeOf(dataLast).toEqualTypeOf<
-      Partial<Record<`prefix_${number}`, string>>
-    >();
+    expectTypeOf(dataLast).toEqualTypeOf<Record<`prefix_${number}`, string>>();
   });
 });
diff --git a/src/split.test.ts b/src/split.test.ts
new file mode 100644
index 0000000..6508415
--- /dev/null
+++ b/src/split.test.ts
@@ -0,0 +1,201 @@
+import { split } from "./split";
+import { pipe } from "./pipe";
+
+describe("runtime", () => {
+  test("empty string, empty separator", () => {
+    expect(split("", "")).toEqual([]);
+  });
+
+  test("empty string, non-empty separator", () => {
+    expect(split("", ",")).toEqual([""]);
+  });
+
+  test("trivial split", () => {
+    expect(split("a", ",")).toEqual(["a"]);
+  });
+
+  test("string contains separator", () => {
+    expect(split(",", ",")).toEqual(["", ""]);
+  });
+
+  test("useful split", () => {
+    expect(split("a,b,c", ",")).toEqual(["a", "b", "c"]);
+  });
+
+  test("regex split", () => {
+    expect(split("a,b,c", /,/u)).toEqual(["a", "b", "c"]);
+  });
+
+  test("multiple types of separators", () => {
+    expect(split("a,b;c", /[;,]/u)).toEqual(["a", "b", "c"]);
+  });
+
+  test("regex with limit", () => {
+    expect(split("a,b,c", /,/u, 2)).toEqual(["a", "b"]);
+  });
+
+  test("limited split", () => {
+    expect(split("a,b,c", ",", 2)).toEqual(["a", "b"]);
+  });
+
+  test("limit is higher than splits", () => {
+    expect(split("a,b,c", ",", 5)).toEqual(["a", "b", "c"]);
+  });
+
+  test("multiple consecutive separators", () => {
+    expect(split("a,,b", ",")).toEqual(["a", "", "b"]);
+  });
+
+  test("separator at the start and end", () => {
+    expect(split(",a,b,", ",")).toEqual(["", "a", "b", ""]);
+  });
+
+  test("empty-string separator", () => {
+    expect(split("abcdef", "")).toEqual(["a", "b", "c", "d", "e", "f"]);
+  });
+
+  test("undefined limit", () => {
+    expect(split("a,b,c", ",", undefined)).toEqual(["a", "b", "c"]);
+  });
+
+  test("negative limit", () => {
+    expect(split("a,b,c", ",", -1)).toEqual(["a", "b", "c"]);
+  });
+
+  test("fractional limits", () => {
+    expect(split("a,b,c", ",", 1.5)).toEqual(["a"]);
+  });
+
+  test("0 limit", () => {
+    expect(split("a,b,c", ",", 0)).toEqual([]);
+  });
+
+  describe("dataLast", () => {
+    test("useful split", () => {
+      expect(pipe("a,b,c", split(","))).toEqual(["a", "b", "c"]);
+    });
+
+    test("regex split", () => {
+      expect(pipe("a,b,c", split(/,/u))).toEqual(["a", "b", "c"]);
+    });
+
+    test("limited split", () => {
+      expect(pipe("a,b,c", split(",", 2))).toEqual(["a", "b"]);
+    });
+
+    test("regex with limit", () => {
+      expect(pipe("a,b,c", split(/,/u, 2))).toEqual(["a", "b"]);
+    });
+
+    test("undefined limit", () => {
+      expect(pipe("a,b,c", split(",", undefined))).toEqual(["a", "b", "c"]);
+    });
+  });
+});
+
+describe("typing", () => {
+  test("non-literals", () => {
+    const result = split("" as string, "" as string);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("non-literal data", () => {
+    const result = split("" as string, ",");
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("non-literal separator", () => {
+    const result = split("", "," as string);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("non-literal limit", () => {
+    const result = split("", "", 1 as number);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("literal empty strings", () => {
+    const result = split("", "");
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+
+  test("trivial literals", () => {
+    const result = split("a", ",");
+    expectTypeOf(result).toEqualTypeOf<["a"]>();
+  });
+
+  test("trivial literals", () => {
+    const result = split("a", ",");
+    expectTypeOf(result).toEqualTypeOf<["a"]>();
+  });
+
+  test("string contains separator", () => {
+    const result = split(",", ",");
+    expectTypeOf(result).toEqualTypeOf<["", ""]>();
+  });
+
+  test("useful split", () => {
+    const result = split("a,b,c", ",");
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("regex split", () => {
+    const result = split("a,b,c", /,/u);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("limited split", () => {
+    const result = split("a,b,c", ",", 2);
+    expectTypeOf(result).toEqualTypeOf<["a", "b"]>();
+  });
+
+  test("limit is higher than splits", () => {
+    const result = split("a,b,c", ",", 5);
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("undefined limit", () => {
+    const result = split("a,b,c", ",", undefined);
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("multiple consecutive separators", () => {
+    const result = split("a,,b", ",");
+    expectTypeOf(result).toEqualTypeOf<["a", "", "b"]>();
+  });
+
+  test("separator at the start and end", () => {
+    const result = split(",a,b,", ",");
+    expectTypeOf(result).toEqualTypeOf<["", "a", "b", ""]>();
+  });
+
+  test("empty string separator", () => {
+    const result = split("abcdef", "");
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c", "d", "e", "f"]>();
+  });
+
+  test("empty everything", () => {
+    const result = split("", "");
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+
+  test("literal string with multiple character separator", () => {
+    const result = split("a--b--c", "--");
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("negative limit", () => {
+    const result = split("a,b,c", ",", -1);
+    expectTypeOf(result).toEqualTypeOf<["a", "b", "c"]>();
+  });
+
+  test("fractional limits", () => {
+    const result = split("a,b,c", ",", 1.5);
+    expectTypeOf(result).toEqualTypeOf<Array<string>>();
+  });
+
+  test("0 limit", () => {
+    const result = split("a,b,c", ",", 0);
+    expectTypeOf(result).toEqualTypeOf<[]>();
+  });
+});
diff --git a/src/values.test.ts b/src/values.test.ts
index 5f79e6b..f2e2c7e 100644
--- a/src/values.test.ts
+++ b/src/values.test.ts
@@ -53,6 +53,14 @@ describe("typing", () => {
     expectTypeOf(result).toEqualTypeOf<Array<boolean>>();
   });
 
+  it("should correctly type union of Records", () => {
+    const result = values({
+      a: "cat",
+    } as Record<PropertyKey, "cat"> | Record<PropertyKey, "dog">);
+
+    expectTypeOf(result).toEqualTypeOf<Array<"cat"> | Array<"dog">>();
+  });
+
   it("should correctly type typed objects", () => {
     const result = values<{ type: "cat" | "dog"; age: number }>({
       type: "cat",