Repository: remeda/remeda
Pull request #1028 (2025-02-17 13:36:56)

chore: implement new vitest lint rule
New vitest lint rule requires typing `vi.fn` usage explicitly.

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

Latest deployment of this branch, based on commit c3b70ad54fb5eede10076318e3b53ce264f4863a:

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


Patch:
diff --git a/package-lock.json b/package-lock.json
index 459e8c6..3942e7f 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,29 +9,29 @@
       "version": "2.0.0",
       "license": "MIT",
       "dependencies": {
-        "type-fest": "^4.33.0"
+        "type-fest": "^4.35.0"
       },
       "devDependencies": {
         "@arethetypeswrong/cli": "^0.17.3",
-        "@eslint/js": "^9.19.0",
+        "@eslint/js": "^9.20.0",
         "@types/eslint__js": "^8.42.3",
         "@types/eslint-config-prettier": "^6.11.3",
-        "@types/node": "^22.13.0",
-        "@vitest/coverage-v8": "^3.0.4",
-        "@vitest/eslint-plugin": "^1.1.25",
-        "eslint": "^9.19.0",
+        "@types/node": "^22.13.4",
+        "@vitest/coverage-v8": "^3.0.5",
+        "@vitest/eslint-plugin": "^1.1.31",
+        "eslint": "^9.20.1",
         "eslint-config-prettier": "^10.0.1",
         "eslint-plugin-jsdoc": "^50.6.3",
         "eslint-plugin-unicorn": "^56.0.1",
         "husky": "^9.1.7",
         "lint-staged": "^15.4.3",
-        "prettier": "^3.4.2",
-        "publint": "^0.3.2",
-        "semantic-release": "^24.2.1",
+        "prettier": "^3.5.1",
+        "publint": "^0.3.5",
+        "semantic-release": "^24.2.3",
         "tsup": "^8.3.6",
         "typescript": "^5.7.3",
-        "typescript-eslint": "^8.22.0",
-        "vitest": "^3.0.4"
+        "typescript-eslint": "^8.24.0",
+        "vitest": "^3.0.5"
       }
     },
     "node_modules/@ampproject/remapping": {
@@ -765,9 +765,9 @@
       }
     },
     "node_modules/@eslint/js": {
-      "version": "9.19.0",
-      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.19.0.tgz",
-      "integrity": "sha512-rbq9/g38qjfqFLOVPvwjIvFFdNziEC5S65jmjPw5r6A//QH+W91akh9irMwjDN8zKUTak6W9EsAv4m/7Wnw0UQ==",
+      "version": "9.20.0",
+      "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.20.0.tgz",
+      "integrity": "sha512-iZA07H9io9Wn836aVTytRaNqh00Sad+EamwOVJT12GTLw1VGMFV/4JaME+JjLtr9fiGaoWgYnS54wrfWsSs4oQ==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -1951,9 +1951,9 @@
       "license": "MIT"
     },
     "node_modules/@types/node": {
-      "version": "22.13.0",
-      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.0.tgz",
-      "integrity": "sha512-ClIbNe36lawluuvq3+YYhnIN2CELi+6q8NpnM7PYp4hBn/TatfboPgVSm2rwKRfnV2M+Ty9GWDFI64KEe+kysA==",
+      "version": "22.13.4",
+      "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.4.tgz",
+      "integrity": "sha512-ywP2X0DYtX3y08eFVx5fNIw7/uIv8hYUKgXoK8oayJlLnKcRfEYCxWMVE1XagUdVtCJlZT1AU4LXEABW+L1Peg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -1975,21 +1975,21 @@
       "license": "MIT"
     },
     "node_modules/@typescript-eslint/eslint-plugin": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.22.0.tgz",
-      "integrity": "sha512-4Uta6REnz/xEJMvwf72wdUnC3rr4jAQf5jnTkeRQ9b6soxLxhDEbS/pfMPoJLDfFPNVRdryqWUIV/2GZzDJFZw==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.24.0.tgz",
+      "integrity": "sha512-aFcXEJJCI4gUdXgoo/j9udUYIHgF23MFkg09LFz2dzEmU0+1Plk4rQWv/IYKvPHAtlkkGoB3m5e6oUp+JPsNaQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/regexpp": "^4.10.0",
-        "@typescript-eslint/scope-manager": "8.22.0",
-        "@typescript-eslint/type-utils": "8.22.0",
-        "@typescript-eslint/utils": "8.22.0",
-        "@typescript-eslint/visitor-keys": "8.22.0",
+        "@typescript-eslint/scope-manager": "8.24.0",
+        "@typescript-eslint/type-utils": "8.24.0",
+        "@typescript-eslint/utils": "8.24.0",
+        "@typescript-eslint/visitor-keys": "8.24.0",
         "graphemer": "^1.4.0",
         "ignore": "^5.3.1",
         "natural-compare": "^1.4.0",
-        "ts-api-utils": "^2.0.0"
+        "ts-api-utils": "^2.0.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2005,16 +2005,16 @@
       }
     },
     "node_modules/@typescript-eslint/parser": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.22.0.tgz",
-      "integrity": "sha512-MqtmbdNEdoNxTPzpWiWnqNac54h8JDAmkWtJExBVVnSrSmi9z+sZUt0LfKqk9rjqmKOIeRhO4fHHJ1nQIjduIQ==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.24.0.tgz",
+      "integrity": "sha512-MFDaO9CYiard9j9VepMNa9MTcqVvSny2N4hkY6roquzj8pdCBRENhErrteaQuu7Yjn1ppk0v1/ZF9CG3KIlrTA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/scope-manager": "8.22.0",
-        "@typescript-eslint/types": "8.22.0",
-        "@typescript-eslint/typescript-estree": "8.22.0",
-        "@typescript-eslint/visitor-keys": "8.22.0",
+        "@typescript-eslint/scope-manager": "8.24.0",
+        "@typescript-eslint/types": "8.24.0",
+        "@typescript-eslint/typescript-estree": "8.24.0",
+        "@typescript-eslint/visitor-keys": "8.24.0",
         "debug": "^4.3.4"
       },
       "engines": {
@@ -2030,14 +2030,14 @@
       }
     },
     "node_modules/@typescript-eslint/scope-manager": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.22.0.tgz",
-      "integrity": "sha512-/lwVV0UYgkj7wPSw0o8URy6YI64QmcOdwHuGuxWIYznO6d45ER0wXUbksr9pYdViAofpUCNJx/tAzNukgvaaiQ==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.24.0.tgz",
+      "integrity": "sha512-HZIX0UByphEtdVBKaQBgTDdn9z16l4aTUz8e8zPQnyxwHBtf5vtl1L+OhH+m1FGV9DrRmoDuYKqzVrvWDcDozw==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.22.0",
-        "@typescript-eslint/visitor-keys": "8.22.0"
+        "@typescript-eslint/types": "8.24.0",
+        "@typescript-eslint/visitor-keys": "8.24.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2048,16 +2048,16 @@
       }
     },
     "node_modules/@typescript-eslint/type-utils": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.22.0.tgz",
-      "integrity": "sha512-NzE3aB62fDEaGjaAYZE4LH7I1MUwHooQ98Byq0G0y3kkibPJQIXVUspzlFOmOfHhiDLwKzMlWxaNv+/qcZurJA==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.24.0.tgz",
+      "integrity": "sha512-8fitJudrnY8aq0F1wMiPM1UUgiXQRJ5i8tFjq9kGfRajU+dbPyOuHbl0qRopLEidy0MwqgTHDt6CnSeXanNIwA==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/typescript-estree": "8.22.0",
-        "@typescript-eslint/utils": "8.22.0",
+        "@typescript-eslint/typescript-estree": "8.24.0",
+        "@typescript-eslint/utils": "8.24.0",
         "debug": "^4.3.4",
-        "ts-api-utils": "^2.0.0"
+        "ts-api-utils": "^2.0.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2072,9 +2072,9 @@
       }
     },
     "node_modules/@typescript-eslint/types": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.22.0.tgz",
-      "integrity": "sha512-0S4M4baNzp612zwpD4YOieP3VowOARgK2EkN/GBn95hpyF8E2fbMT55sRHWBq+Huaqk3b3XK+rxxlM8sPgGM6A==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.24.0.tgz",
+      "integrity": "sha512-VacJCBTyje7HGAw7xp11q439A+zeGG0p0/p2zsZwpnMzjPB5WteaWqt4g2iysgGFafrqvyLWqq6ZPZAOCoefCw==",
       "dev": true,
       "license": "MIT",
       "engines": {
@@ -2086,20 +2086,20 @@
       }
     },
     "node_modules/@typescript-eslint/typescript-estree": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.22.0.tgz",
-      "integrity": "sha512-SJX99NAS2ugGOzpyhMza/tX+zDwjvwAtQFLsBo3GQxiGcvaKlqGBkmZ+Y1IdiSi9h4Q0Lr5ey+Cp9CGWNY/F/w==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.24.0.tgz",
+      "integrity": "sha512-ITjYcP0+8kbsvT9bysygfIfb+hBj6koDsu37JZG7xrCiy3fPJyNmfVtaGsgTUSEuTzcvME5YI5uyL5LD1EV5ZQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.22.0",
-        "@typescript-eslint/visitor-keys": "8.22.0",
+        "@typescript-eslint/types": "8.24.0",
+        "@typescript-eslint/visitor-keys": "8.24.0",
         "debug": "^4.3.4",
         "fast-glob": "^3.3.2",
         "is-glob": "^4.0.3",
         "minimatch": "^9.0.4",
         "semver": "^7.6.0",
-        "ts-api-utils": "^2.0.0"
+        "ts-api-utils": "^2.0.1"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2113,16 +2113,16 @@
       }
     },
     "node_modules/@typescript-eslint/utils": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.22.0.tgz",
-      "integrity": "sha512-T8oc1MbF8L+Bk2msAvCUzjxVB2Z2f+vXYfcucE2wOmYs7ZUwco5Ep0fYZw8quNwOiw9K8GYVL+Kgc2pETNTLOg==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.24.0.tgz",
+      "integrity": "sha512-07rLuUBElvvEb1ICnafYWr4hk8/U7X9RDCOqd9JcAMtjh/9oRmcfN4yGzbPVirgMR0+HLVHehmu19CWeh7fsmQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.4.0",
-        "@typescript-eslint/scope-manager": "8.22.0",
-        "@typescript-eslint/types": "8.22.0",
-        "@typescript-eslint/typescript-estree": "8.22.0"
+        "@typescript-eslint/scope-manager": "8.24.0",
+        "@typescript-eslint/types": "8.24.0",
+        "@typescript-eslint/typescript-estree": "8.24.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -2137,13 +2137,13 @@
       }
     },
     "node_modules/@typescript-eslint/visitor-keys": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.22.0.tgz",
-      "integrity": "sha512-AWpYAXnUgvLNabGTy3uBylkgZoosva/miNd1I8Bz3SjotmQPbVqhO4Cczo8AsZ44XVErEBPr/CRSgaj8sG7g0w==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.24.0.tgz",
+      "integrity": "sha512-kArLq83QxGLbuHrTMoOEWO+l2MwsNS2TGISEdx8xgqpkbytB07XmlQyQdNDrCc1ecSqx0cnmhGvpX+VBwqqSkg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/types": "8.22.0",
+        "@typescript-eslint/types": "8.24.0",
         "eslint-visitor-keys": "^4.2.0"
       },
       "engines": {
@@ -2201,9 +2201,9 @@
       }
     },
     "node_modules/@vitest/eslint-plugin": {
-      "version": "1.1.25",
-      "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.25.tgz",
-      "integrity": "sha512-u8DpDnMbPcqBmJOB4PeEtn6q7vKmLVTLFMpzoxSAo0hjYdl4iYSHRleqwPQo0ywc7UV0S6RKIahYRQ3BnZdMVw==",
+      "version": "1.1.31",
+      "resolved": "https://registry.npmjs.org/@vitest/eslint-plugin/-/eslint-plugin-1.1.31.tgz",
+      "integrity": "sha512-xlsLr+e+AXZ/00eVZCtNmMeCJoJaRCoLDiAgLcxgQjSS1EertieB2MUHf8xIqPKs9lECc/UpL+y1xDcpvi02hw==",
       "dev": true,
       "license": "MIT",
       "peerDependencies": {
@@ -3426,18 +3426,18 @@
       }
     },
     "node_modules/eslint": {
-      "version": "9.19.0",
-      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.19.0.tgz",
-      "integrity": "sha512-ug92j0LepKlbbEv6hD911THhoRHmbdXt2gX+VDABAW/Ir7D3nqKdv5Pf5vtlyY6HQMTEP2skXY43ueqTCWssEA==",
+      "version": "9.20.1",
+      "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.20.1.tgz",
+      "integrity": "sha512-m1mM33o6dBUjxl2qb6wv6nGNwCAsns1eKtaQ4l/NPHeTvhiUPbtdfMyktxN4B3fgHIgsYh1VT3V9txblpQHq+g==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@eslint-community/eslint-utils": "^4.2.0",
         "@eslint-community/regexpp": "^4.12.1",
         "@eslint/config-array": "^0.19.0",
-        "@eslint/core": "^0.10.0",
+        "@eslint/core": "^0.11.0",
         "@eslint/eslintrc": "^3.2.0",
-        "@eslint/js": "9.19.0",
+        "@eslint/js": "9.20.0",
         "@eslint/plugin-kit": "^0.2.5",
         "@humanfs/node": "^0.16.6",
         "@humanwhocodes/module-importer": "^1.0.1",
@@ -3601,6 +3601,19 @@
         "url": "https://opencollective.com/eslint"
       }
     },
+    "node_modules/eslint/node_modules/@eslint/core": {
+      "version": "0.11.0",
+      "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.11.0.tgz",
+      "integrity": "sha512-DWUB2pksgNEb6Bz2fggIy1wh6fGgZP4Xyy/Mt0QZPiloKKXerbqq9D3SBQTlCRYOrcRPu4vuz+CGjwdfqxnoWA==",
+      "dev": true,
+      "license": "Apache-2.0",
+      "dependencies": {
+        "@types/json-schema": "^7.0.15"
+      },
+      "engines": {
+        "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
+      }
+    },
     "node_modules/eslint/node_modules/brace-expansion": {
       "version": "1.1.11",
       "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz",
@@ -8769,9 +8782,9 @@
       }
     },
     "node_modules/prettier": {
-      "version": "3.4.2",
-      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz",
-      "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==",
+      "version": "3.5.1",
+      "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.5.1.tgz",
+      "integrity": "sha512-hPpFQvHwL3Qv5AdRvBFMhnKo4tYxp0ReXiPn2bxkiohEX6mBeBwEpBSQTkD458RaaDKQMYSp4hX4UtfUTA5wDw==",
       "dev": true,
       "license": "MIT",
       "bin": {
@@ -8815,14 +8828,14 @@
       "license": "ISC"
     },
     "node_modules/publint": {
-      "version": "0.3.2",
-      "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.2.tgz",
-      "integrity": "sha512-fPs7QUbUvwixxPYUUTn0Kqp0rbH5rbiAOZwQOXMkIj+4Nopby1AngodSQmzTkJWTJ5R4uVV8oYmgVIjj+tgv1w==",
+      "version": "0.3.5",
+      "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.5.tgz",
+      "integrity": "sha512-/84pl/T/emCA5hHmNYqsE/x0Voikg278QmFwNiORYqnZgqeII2HSZ+aAGs4frfDpOCQlU1SAgYloz8ayJGMbIg==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
         "@publint/pack": "^0.1.1",
-        "package-manager-detector": "^0.2.8",
+        "package-manager-detector": "^0.2.9",
         "picocolors": "^1.1.1",
         "sade": "^1.8.1"
       },
@@ -9348,9 +9361,9 @@
       "license": "MIT"
     },
     "node_modules/semantic-release": {
-      "version": "24.2.1",
-      "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.1.tgz",
-      "integrity": "sha512-z0/3cutKNkLQ4Oy0HTi3lubnjTsdjjgOqmxdPjeYWe6lhFqUPfwslZxRHv3HDZlN4MhnZitb9SLihDkZNxOXfQ==",
+      "version": "24.2.3",
+      "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.3.tgz",
+      "integrity": "sha512-KRhQG9cUazPavJiJEFIJ3XAMjgfd0fcK3B+T26qOl8L0UG5aZUjeRfREO0KM5InGtYwxqiiytkJrbcYoLDEv0A==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
@@ -10695,9 +10708,9 @@
       }
     },
     "node_modules/type-fest": {
-      "version": "4.33.0",
-      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.33.0.tgz",
-      "integrity": "sha512-s6zVrxuyKbbAsSAD5ZPTB77q4YIdRctkTbJ2/Dqlinwz+8ooH2gd+YA7VA6Pa93KML9GockVvoxjZ2vHP+mu8g==",
+      "version": "4.35.0",
+      "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.35.0.tgz",
+      "integrity": "sha512-2/AwEFQDFEy30iOLjrvHDIH7e4HEWH+f1Yl1bI5XMqzuoCUqwYCdxachgsgv0og/JdVZUhbfjcJAoHj5L1753A==",
       "license": "(MIT OR CC0-1.0)",
       "engines": {
         "node": ">=16"
@@ -10721,15 +10734,15 @@
       }
     },
     "node_modules/typescript-eslint": {
-      "version": "8.22.0",
-      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.22.0.tgz",
-      "integrity": "sha512-Y2rj210FW1Wb6TWXzQc5+P+EWI9/zdS57hLEc0gnyuvdzWo8+Y8brKlbj0muejonhMI/xAZCnZZwjbIfv1CkOw==",
+      "version": "8.24.0",
+      "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.24.0.tgz",
+      "integrity": "sha512-/lmv4366en/qbB32Vz5+kCNZEMf6xYHwh1z48suBwZvAtnXKbP+YhGe8OLE2BqC67LMqKkCNLtjejdwsdW6uOQ==",
       "dev": true,
       "license": "MIT",
       "dependencies": {
-        "@typescript-eslint/eslint-plugin": "8.22.0",
-        "@typescript-eslint/parser": "8.22.0",
-        "@typescript-eslint/utils": "8.22.0"
+        "@typescript-eslint/eslint-plugin": "8.24.0",
+        "@typescript-eslint/parser": "8.24.0",
+        "@typescript-eslint/utils": "8.24.0"
       },
       "engines": {
         "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
diff --git a/package.json b/package.json
index 9373bd7..70213a8 100644
--- a/package.json
+++ b/package.json
@@ -61,28 +61,28 @@
     "test:typing": "vitest --typecheck.only --typecheck.ignoreSourceErrors"
   },
   "dependencies": {
-    "type-fest": "^4.33.0"
+    "type-fest": "^4.35.0"
   },
   "devDependencies": {
     "@arethetypeswrong/cli": "^0.17.3",
-    "@eslint/js": "^9.19.0",
+    "@eslint/js": "^9.20.0",
     "@types/eslint__js": "^8.42.3",
     "@types/eslint-config-prettier": "^6.11.3",
-    "@types/node": "^22.13.0",
-    "@vitest/coverage-v8": "^3.0.4",
-    "@vitest/eslint-plugin": "^1.1.25",
-    "eslint": "^9.19.0",
+    "@types/node": "^22.13.4",
+    "@vitest/coverage-v8": "^3.0.5",
+    "@vitest/eslint-plugin": "^1.1.31",
+    "eslint": "^9.20.1",
     "eslint-config-prettier": "^10.0.1",
     "eslint-plugin-jsdoc": "^50.6.3",
     "eslint-plugin-unicorn": "^56.0.1",
     "husky": "^9.1.7",
     "lint-staged": "^15.4.3",
-    "prettier": "^3.4.2",
-    "publint": "^0.3.2",
-    "semantic-release": "^24.2.1",
+    "prettier": "^3.5.1",
+    "publint": "^0.3.5",
+    "semantic-release": "^24.2.3",
     "tsup": "^8.3.6",
     "typescript": "^5.7.3",
-    "typescript-eslint": "^8.22.0",
-    "vitest": "^3.0.4"
+    "typescript-eslint": "^8.24.0",
+    "vitest": "^3.0.5"
   }
 }


Test patch:
diff --git a/src/debounce.test.ts b/src/debounce.test.ts
index 57f3ad7..5d6d263 100644
--- a/src/debounce.test.ts
+++ b/src/debounce.test.ts
@@ -1,11 +1,12 @@
 /* eslint-disable @typescript-eslint/no-deprecated -- We know! */
 
+import { constant } from "./constant";
 import { debounce } from "./debounce";
 import { identity } from "./identity";
 
 describe("main functionality", () => {
   it("should debounce a function", async () => {
-    const mockFn = vi.fn(identity());
+    const mockFn = vi.fn<(x: string) => string>(identity());
 
     const debouncer = debounce(mockFn, { waitMs: 32 });
 
@@ -45,7 +46,7 @@ describe("main functionality", () => {
   });
 
   it("should not immediately call `func` when `wait` is `0`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
 
     const debouncer = debounce(mockFn, {});
 
@@ -60,7 +61,7 @@ describe("main functionality", () => {
   });
 
   it("should apply default options", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
 
     const debouncer = debounce(mockFn, { waitMs: 32 });
 
@@ -74,8 +75,8 @@ describe("main functionality", () => {
   });
 
   it("should support a `leading` option", async () => {
-    const leadingMockFn = vi.fn();
-    const bothMockFn = vi.fn();
+    const leadingMockFn = vi.fn<() => void>();
+    const bothMockFn = vi.fn<() => void>();
 
     const withLeading = debounce(leadingMockFn, {
       waitMs: 32,
@@ -122,7 +123,7 @@ describe("main functionality", () => {
   });
 
   it("should support a `trailing` option", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
 
     const withTrailing = debounce(mockFn, { waitMs: 32, timing: "trailing" });
 
@@ -138,7 +139,7 @@ describe("main functionality", () => {
 
 describe("optional param maxWaitMs", () => {
   it("should support a `maxWait` option", async () => {
-    const mockFn = vi.fn(identity());
+    const mockFn = vi.fn<(x: string) => void>();
 
     const debouncer = debounce(mockFn, { waitMs: 32, maxWaitMs: 64 });
 
@@ -162,8 +163,8 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("should support `maxWait` in a tight loop", async () => {
-    const withMockFn = vi.fn();
-    const withoutMockFn = vi.fn();
+    const withMockFn = vi.fn<() => void>();
+    const withoutMockFn = vi.fn<() => void>();
 
     const withMaxWait = debounce(withMockFn, { waitMs: 32, maxWaitMs: 128 });
     const withoutMaxWait = debounce(withoutMockFn, { waitMs: 96 });
@@ -181,7 +182,7 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("should queue a trailing call for subsequent debounced calls after `maxWait`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
 
     const debouncer = debounce(mockFn, { waitMs: 200, maxWaitMs: 200 });
 
@@ -203,7 +204,7 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("should cancel `maxDelayed` when `delayed` is invoked", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
 
     const debouncer = debounce(mockFn, { waitMs: 32, maxWaitMs: 64 });
 
@@ -220,7 +221,7 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("works like a leaky bucket when only maxWaitMs is set", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
 
     const debouncer = debounce(mockFn, { maxWaitMs: 32 });
 
@@ -258,8 +259,7 @@ describe("additional functionality", () => {
   });
 
   it("can cancel the timer", async () => {
-    const data = "Hello, World!";
-    const mockFn = vi.fn(() => data);
+    const mockFn = vi.fn<() => string>(constant("Hello, World!"));
     const debouncer = debounce(mockFn, { waitMs: 32 });
 
     expect(debouncer.call()).toBeUndefined();
@@ -279,7 +279,7 @@ describe("additional functionality", () => {
 
     await sleep(32);
 
-    expect(debouncer.call()).toStrictEqual(data);
+    expect(debouncer.call()).toBe("Hello, World!");
     expect(mockFn).toHaveBeenCalledTimes(1);
   });
 
diff --git a/src/difference.test.ts b/src/difference.test.ts
index 5cdcc1f..c9cf3c1 100644
--- a/src/difference.test.ts
+++ b/src/difference.test.ts
@@ -66,7 +66,7 @@ it("compares objects by reference", () => {
 });
 
 test("lazy", () => {
-  const mock = vi.fn(identity());
+  const mock = vi.fn<(x: number) => number>(identity());
   const result = pipe(
     [1, 2, 3, 4, 5, 6],
     map(mock),
diff --git a/src/drop.test.ts b/src/drop.test.ts
index 0ce668a..7f78910 100644
--- a/src/drop.test.ts
+++ b/src/drop.test.ts
@@ -1,5 +1,4 @@
 import { drop } from "./drop";
-import { identity } from "./identity";
 import { map } from "./map";
 import { pipe } from "./pipe";
 import { take } from "./take";
@@ -48,7 +47,7 @@ describe("data last", () => {
   });
 
   test("lazy impl", () => {
-    const mockFunc = vi.fn(identity());
+    const mockFunc = vi.fn<(x: number) => number>();
     pipe([1, 2, 3, 4, 5], map(mockFunc), drop(2), take(2));
 
     expect(mockFunc).toHaveBeenCalledTimes(4);
diff --git a/src/evolve.test.ts b/src/evolve.test.ts
index c929ec6..2706490 100644
--- a/src/evolve.test.ts
+++ b/src/evolve.test.ts
@@ -97,7 +97,7 @@ describe("data first", () => {
   });
 
   it("doesn't evolve symbol keys", () => {
-    const mock = vi.fn();
+    const mock = vi.fn<(x: string) => unknown>();
     const mySymbol = Symbol("a");
     // @ts-expect-error [ts2418] - We want to test the runtime even if the typing prevents it.
     evolve({ [mySymbol]: "hello" }, { [mySymbol]: mock });
diff --git a/src/filter.test.ts b/src/filter.test.ts
index 77a1ccb..99436f1 100644
--- a/src/filter.test.ts
+++ b/src/filter.test.ts
@@ -1,4 +1,5 @@
 import { filter } from "./filter";
+import { identity } from "./identity";
 import { map } from "./map";
 import { pipe } from "./pipe";
 
@@ -24,7 +25,7 @@ describe("data_first", () => {
 
 describe("data_last", () => {
   it("filter", () => {
-    const counter = vi.fn((x: unknown) => x);
+    const counter = vi.fn<(x: number) => number>(identity());
     const result = pipe(
       [1, 2, 3],
       filter((x) => x % 2 === 1),
@@ -36,7 +37,7 @@ describe("data_last", () => {
   });
 
   it("filter with typescript guard", () => {
-    const counter = vi.fn((x: unknown) => x);
+    const counter = vi.fn<(x: number) => number>(identity());
     const result = pipe(
       [1, 2, 3, false, "text"],
       filter(isNumber),
@@ -48,7 +49,7 @@ describe("data_last", () => {
   });
 
   it("filter indexed", () => {
-    const counter = vi.fn((x: unknown) => x);
+    const counter = vi.fn<(x: number) => void>(identity());
     const result = pipe(
       [1, 2, 3],
       filter((x, i) => x % 2 === 1 && i !== 1),
diff --git a/src/find.test.ts b/src/find.test.ts
index c649448..71b4140 100644
--- a/src/find.test.ts
+++ b/src/find.test.ts
@@ -1,4 +1,5 @@
 import { find } from "./find";
+import { identity } from "./identity";
 import { map } from "./map";
 import { pipe } from "./pipe";
 
@@ -34,15 +35,18 @@ describe("data first", () => {
 
 describe("data last", () => {
   test("find", () => {
-    const counter = vi.fn((x: { readonly a: number; readonly b: number }) => x);
+    const data = [
+      { a: 1, b: 1 },
+      { a: 1, b: 2 },
+      { a: 2, b: 1 },
+      { a: 1, b: 3 },
+    ] as const;
+
+    const counter =
+      vi.fn<(x: (typeof data)[number]) => (typeof data)[number]>(identity());
 
     const actual = pipe(
-      [
-        { a: 1, b: 1 },
-        { a: 1, b: 2 },
-        { a: 2, b: 1 },
-        { a: 1, b: 3 },
-      ],
+      data,
       map(counter),
       find(({ b }) => b === 2),
     );
@@ -52,15 +56,18 @@ describe("data last", () => {
   });
 
   test("indexed", () => {
-    const counter = vi.fn((x: { readonly a: number; readonly b: number }) => x);
+    const data = [
+      { a: 1, b: 1 },
+      { a: 1, b: 2 },
+      { a: 2, b: 1 },
+      { a: 1, b: 3 },
+    ] as const;
+
+    const counter =
+      vi.fn<(x: (typeof data)[number]) => (typeof data)[number]>(identity());
 
     const actual = pipe(
-      [
-        { a: 1, b: 1 },
-        { a: 1, b: 2 },
-        { a: 2, b: 1 },
-        { a: 1, b: 3 },
-      ],
+      data,
       map(counter),
       find(({ b }, idx) => b === 2 && idx === 1),
     );
diff --git a/src/flat.test.ts b/src/flat.test.ts
index 14277d8..efe7b92 100644
--- a/src/flat.test.ts
+++ b/src/flat.test.ts
@@ -88,8 +88,9 @@ describe("dataLast", () => {
   });
 
   it("works lazily (shallow)", () => {
-    const beforeMock: <T>(x: T) => T = vi.fn(identity());
-    const afterMock: <T>(x: T) => T = vi.fn(identity());
+    // eslint-disable-next-line vitest/require-mock-type-parameters -- TODO: It's hard to type this correctly taking into account the deep structure of the array. We might be able to work around that if we pull the array out and use it's type (e.g. typeof) to build the type for the function here...
+    const beforeMock = vi.fn(identity());
+    const afterMock = vi.fn<(x: number) => number>(identity());
     const result = pipe(
       [[1, 2], 3, [4, 5]],
       map(beforeMock),
@@ -104,8 +105,9 @@ describe("dataLast", () => {
   });
 
   it("works lazily (deep)", () => {
-    const beforeMock: <T>(x: T) => T = vi.fn(identity());
-    const afterMock: <T>(x: T) => T = vi.fn(identity());
+    // eslint-disable-next-line vitest/require-mock-type-parameters -- TODO: It's hard to type this correctly taking into account the deep structure of the array. We might be able to work around that if we pull the array out and use it's type (e.g. typeof) to build the type for the function here...
+    const beforeMock = vi.fn(identity());
+    const afterMock = vi.fn<(x: number) => number>(identity());
     const result = pipe(
       [[[0]], [[[1, 2], [[3]], [[4, 5]]]], 6],
       map(beforeMock),
diff --git a/src/forEach.test.ts b/src/forEach.test.ts
index 6c0dfbf..0c86b73 100644
--- a/src/forEach.test.ts
+++ b/src/forEach.test.ts
@@ -4,7 +4,7 @@ import { take } from "./take";
 
 test("dataFirst", () => {
   const data = [1, 2, 3];
-  const cb = vi.fn();
+  const cb = vi.fn<(x: number) => void>();
 
   forEach(data, cb);
 
@@ -15,7 +15,7 @@ test("dataFirst", () => {
 
 test("dataLast", () => {
   const data = [1, 2, 3];
-  const cb = vi.fn();
+  const cb = vi.fn<(x: number) => void>();
 
   // Because the callback is used before forEach "sees" `data`, we need to
   // explicitly tell it the how to type the `data` param..
@@ -32,9 +32,7 @@ test("dataLast", () => {
 test("pipe", () => {
   const data = [1, 2, 3];
 
-  // Callbacks take their type from the pipe itself, but because we construct
-  // it here outside of the pipe, we need to deliberately type it.
-  const cb = vi.fn((x: number) => x);
+  const cb = vi.fn<(x: number) => void>();
 
   const result = pipe(data, forEach(cb));
 
@@ -48,7 +46,7 @@ test("pipe", () => {
 });
 
 test("with take", () => {
-  const count = vi.fn();
+  const count = vi.fn<() => void>();
   const result = pipe(
     [1, 2, 3],
     forEach(() => {
diff --git a/src/forEachObj.test.ts b/src/forEachObj.test.ts
index 61c67d7..76c4529 100644
--- a/src/forEachObj.test.ts
+++ b/src/forEachObj.test.ts
@@ -7,7 +7,7 @@ test("dataFirst", () => {
     b: 2,
     c: 3,
   };
-  const cb = vi.fn();
+  const cb = vi.fn<(value: number, key: string) => void>();
 
   forEachObj(data, cb);
 
@@ -18,7 +18,7 @@ test("dataFirst", () => {
 
 test("doesn't run on symbol keys", () => {
   const data = { [Symbol("a")]: 4 };
-  const cb = vi.fn();
+  const cb = vi.fn<(value: never, key: never) => void>();
 
   forEachObj(data, cb);
 
@@ -35,7 +35,7 @@ test("number keys are translated to string", () => {
 
 test("dataLast", () => {
   const data = { a: 1, b: 2, c: 3 };
-  const cb = vi.fn();
+  const cb = vi.fn<(value: number, key: string) => void>();
 
   expect(pipe(data, forEachObj(cb))).toBe(data);
   expect(cb).toHaveBeenCalledWith(1, "a", data);
diff --git a/src/funnel.lodash-debounce-with-cached-value.test.ts b/src/funnel.lodash-debounce-with-cached-value.test.ts
index 6a26f05..8c812bb 100644
--- a/src/funnel.lodash-debounce-with-cached-value.test.ts
+++ b/src/funnel.lodash-debounce-with-cached-value.test.ts
@@ -110,7 +110,7 @@ const UT = 16;
 
 describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", () => {
   it("should debounce a function", async () => {
-    const mockFn = vi.fn(identity());
+    const mockFn = vi.fn<(x: string) => string>(identity());
     const debounced = debounceWithCachedValue(mockFn, UT);
 
     expect(debounced("a")).toBeUndefined();
@@ -160,7 +160,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
 
   it("should invoke the trailing call with the correct arguments and `this` binding", async () => {
     const DATA = {};
-    const mockFn = vi.fn(constant(false));
+    const mockFn = vi.fn<(a: object, b: string) => boolean>(constant(false));
 
     // In Lodash the test uses both `leading` and `trailing` timing options
     // for this test, but it only works because the `leading` option in Lodash
@@ -227,7 +227,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should noop `cancel` and `flush` when nothing is queued", async () => {
-    const mockFn = vi.fn(constant("hello"));
+    const mockFn = vi.fn<() => string>(constant("hello"));
     const debounced = debounceWithCachedValue(mockFn, UT);
     debounced.cancel();
 
diff --git a/src/funnel.lodash-debounce.test.ts b/src/funnel.lodash-debounce.test.ts
index 9fe9e0d..286d0c6 100644
--- a/src/funnel.lodash-debounce.test.ts
+++ b/src/funnel.lodash-debounce.test.ts
@@ -97,7 +97,7 @@ const UT = 16;
 
 describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", () => {
   it("should debounce a function", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: string) => void>();
     const debounced = debounce(mockFn, UT);
     debounced("a");
     debounced("b");
@@ -122,7 +122,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should not immediately call `func` when `wait` is `0`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, 0);
     debounced();
     debounced();
@@ -135,7 +135,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should apply default options", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, UT, {});
     debounced();
 
@@ -147,8 +147,8 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should support a `leading` option", async () => {
-    const mockWithLeading = vi.fn();
-    const mockWithLeadingAndTrailing = vi.fn();
+    const mockWithLeading = vi.fn<() => void>();
+    const mockWithLeadingAndTrailing = vi.fn<() => void>();
     const withLeading = debounce(mockWithLeading, UT, {
       leading: true,
       // This Lodash test configures both debouncers with the same timing
@@ -180,8 +180,8 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should support a `trailing` option", async () => {
-    const mockWith = vi.fn();
-    const mockWithout = vi.fn();
+    const mockWith = vi.fn<() => void>();
+    const mockWithout = vi.fn<() => void>();
     const withTrailing = debounce(mockWith, UT, { trailing: true });
     // The lodash test disabled `trailing` but doesn't enable `leading`
     // resulting in a debouncer that has no effective timing policy. We handle
@@ -203,7 +203,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should support a `maxWait` option", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, UT, { maxWait: 2 * UT });
     debounced();
     debounced();
@@ -225,8 +225,8 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should support `maxWait` in a tight loop", async () => {
-    const mockWith = vi.fn();
-    const mockWithout = vi.fn();
+    const mockWith = vi.fn<() => void>();
+    const mockWithout = vi.fn<() => void>();
     const withMaxWait = debounce(mockWith, 2 * UT, { maxWait: 4 * UT });
     const withoutMaxWait = debounce(mockWithout, 3 * UT);
     const end = Date.now() + 10 * UT;
@@ -256,7 +256,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should queue a trailing call for subsequent debounced calls after `maxWait`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, 6 * UT, { maxWait: 6 * UT });
     debounced();
     await sleep(5.5 * UT);
@@ -271,7 +271,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should cancel `maxDelayed` when `delayed` is invoked", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, UT, { maxWait: 2 * UT });
     debounced();
     await sleep(4 * UT);
@@ -285,7 +285,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
   });
 
   it("should invoke the trailing call with the correct arguments and `this` binding", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(a: object, b: string) => void>();
     const DATA = {};
     const debounced = debounce(mockFn, UT, {
       leading: true,
@@ -305,7 +305,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L4187", ()
 
 describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", () => {
   it("should use a default `wait` of `0`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn);
     debounced();
     await sleep(UT);
@@ -334,7 +334,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should support cancelling delayed calls", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, UT, { leading: false });
     debounced();
     debounced.cancel();
@@ -344,7 +344,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should reset `lastCalled` after cancelling", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, UT, { leading: true });
     debounced();
 
@@ -362,7 +362,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should support flushing delayed calls", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, UT, { leading: false });
     debounced();
     debounced.flush();
@@ -375,7 +375,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should noop `cancel` and `flush` when nothing is queued", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debounced = debounce(mockFn, UT);
     debounced.cancel();
     debounced.flush();
diff --git a/src/funnel.lodash-throttle-with-cached-value.test.ts b/src/funnel.lodash-throttle-with-cached-value.test.ts
index 6cfb970..777f26d 100644
--- a/src/funnel.lodash-throttle-with-cached-value.test.ts
+++ b/src/funnel.lodash-throttle-with-cached-value.test.ts
@@ -133,8 +133,8 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should support a `trailing` option", async () => {
-    const mockWith = vi.fn(identity() as <T>(x: T) => T);
-    const mockWithout = vi.fn(identity() as <T>(x: T) => T);
+    const mockWith = vi.fn<<T>(x: T) => T>(identity());
+    const mockWithout = vi.fn<<T>(x: T) => T>(identity());
     const withTrailing = throttleWithCachedValue(mockWith, 2 * UT, {
       trailing: true,
     });
@@ -198,7 +198,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should noop `cancel` and `flush` when nothing is queued", async () => {
-    const mockFn = vi.fn(constant("hello"));
+    const mockFn = vi.fn<() => string>(constant("hello"));
     const throttled = throttleWithCachedValue(mockFn, UT);
     throttled.cancel();
 
diff --git a/src/funnel.lodash-throttle.test.ts b/src/funnel.lodash-throttle.test.ts
index 64ec3cb..ac7b287 100644
--- a/src/funnel.lodash-throttle.test.ts
+++ b/src/funnel.lodash-throttle.test.ts
@@ -91,7 +91,7 @@ const UT = 16;
 
 describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", () => {
   it("should throttle a function", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT);
     throttled();
     throttled();
@@ -106,7 +106,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should clear timeout when `func` is called", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT);
     throttled();
 
@@ -120,7 +120,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should not trigger a trailing call when invoked once", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT);
     throttled();
 
@@ -132,7 +132,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should trigger a call when invoked repeatedly", () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
 
     const throttled = throttle(mockFn, UT);
 
@@ -145,7 +145,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should trigger a call when invoked repeatedly and `leading` is `false`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT, { leading: false });
     const end = Date.now() + 10 * UT;
     while (Date.now() < end) {
@@ -158,7 +158,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should trigger a second throttled call as soon as possible", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, 4 * UT, { leading: false });
     throttled();
     await sleep(6 * UT);
@@ -176,7 +176,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should apply default options", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT, {});
     throttled();
     throttled();
@@ -189,8 +189,8 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should support a `leading` option", () => {
-    const mockWith = vi.fn();
-    const mockWithout = vi.fn();
+    const mockWith = vi.fn<() => void>();
+    const mockWithout = vi.fn<() => void>();
     const withLeading = throttle(mockWith, UT, { leading: true });
     const withoutLeading = throttle(mockWithout, UT, { leading: false });
 
@@ -202,8 +202,8 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should support a `trailing` option", async () => {
-    const mockWith = vi.fn();
-    const mockWithout = vi.fn();
+    const mockWith = vi.fn<(x: string) => void>();
+    const mockWithout = vi.fn<(x: string) => void>();
     const withTrailing = throttle(mockWith, 2 * UT, { trailing: true });
     const withoutTrailing = throttle(mockWithout, 2 * UT, { trailing: false });
     withTrailing("a");
@@ -223,7 +223,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
   });
 
   it("should not update `lastCalled`, at the end of the timeout, when `trailing` is `false`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, 64, { trailing: false });
     throttled();
     throttled();
@@ -238,7 +238,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L22768", ()
 
 describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", () => {
   it("should use a default `wait` of `0`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn);
     throttled();
     await sleep(UT);
@@ -267,7 +267,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should support cancelling delayed calls", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT, { leading: false });
     throttled();
     throttled.cancel();
@@ -277,7 +277,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should reset `lastCalled` after cancelling", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT, { leading: true });
     throttled();
 
@@ -295,7 +295,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should support flushing delayed calls", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT, { leading: false });
     throttled();
     throttled.flush();
@@ -308,7 +308,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
   });
 
   it("should noop `cancel` and `flush` when nothing is queued", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT);
     throttled.cancel();
     throttled.flush();
@@ -323,7 +323,7 @@ describe("https://github.com/lodash/lodash/blob/4.17.21/test/test.js#L23038", ()
 
 describe("Not tested by Lodash", () => {
   it("should do nothing when `leading` and `trailing` are both `disabled`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const throttled = throttle(mockFn, UT, { leading: false, trailing: false });
     throttled();
     throttled();
diff --git a/src/funnel.reference-batch.test.ts b/src/funnel.reference-batch.test.ts
index 492c28d..baa1037 100644
--- a/src/funnel.reference-batch.test.ts
+++ b/src/funnel.reference-batch.test.ts
@@ -104,10 +104,9 @@ function batch<Params extends Array<any>, BatchResponse, Result>(
 
 describe("showcase", () => {
   test("results as object", async () => {
-    const mockApi = vi.fn(
-      async (words: ReadonlyArray<string>): Promise<Record<string, number>> =>
-        fromKeys(words, (word) => word.length),
-    );
+    const mockApi = vi.fn<
+      (words: ReadonlyArray<string>) => Promise<Record<string, number>>
+    >(async (words) => fromKeys(words, (word) => word.length));
 
     const countLettersApi = batch(
       // We only need to type the `requests` param of the `executor` callback.
@@ -144,10 +143,9 @@ describe("showcase", () => {
   });
 
   test("results as array", async () => {
-    const mockApi = vi.fn(
-      async (words: ReadonlyArray<string>): Promise<ReadonlyArray<number>> =>
-        words.map((word) => word.length),
-    );
+    const mockApi = vi.fn<
+      (words: ReadonlyArray<string>) => Promise<ReadonlyArray<number>>
+    >(async (words) => words.map((word) => word.length));
 
     const countLettersApi = batch(
       // We only need to type the `requests` param of the `executor` callback.
diff --git a/src/funnel.remeda-debounce.test.ts b/src/funnel.remeda-debounce.test.ts
index 3b0a3b6..e3982bd 100644
--- a/src/funnel.remeda-debounce.test.ts
+++ b/src/funnel.remeda-debounce.test.ts
@@ -3,6 +3,7 @@
  */
 
 import { sleep } from "../test/sleep";
+import { constant } from "./constant";
 import { funnel } from "./funnel";
 import { identity } from "./identity";
 
@@ -109,7 +110,7 @@ function debounce<F extends (...args: any) => any>(
 
 describe("main functionality", () => {
   it("should debounce a function", async () => {
-    const mockFn = vi.fn(identity());
+    const mockFn = vi.fn<(x: string) => string>(identity());
     const debouncer = debounce(mockFn, { waitMs: 32 });
 
     expect([
@@ -147,7 +148,7 @@ describe("main functionality", () => {
   });
 
   it("should not immediately call `func` when `wait` is `0`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debouncer = debounce(mockFn, {});
     debouncer.call();
     debouncer.call();
@@ -160,7 +161,7 @@ describe("main functionality", () => {
   });
 
   it("should apply default options", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debouncer = debounce(mockFn, { waitMs: 32 });
     debouncer.call();
 
@@ -172,8 +173,8 @@ describe("main functionality", () => {
   });
 
   it("should support a `leading` option", async () => {
-    const leadingMockFn = vi.fn();
-    const bothMockFn = vi.fn();
+    const leadingMockFn = vi.fn<() => void>();
+    const bothMockFn = vi.fn<() => void>();
     const withLeading = debounce(leadingMockFn, {
       waitMs: 32,
       timing: "leading",
@@ -217,7 +218,7 @@ describe("main functionality", () => {
   });
 
   it("should support a `trailing` option", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const withTrailing = debounce(mockFn, { waitMs: 32, timing: "trailing" });
     withTrailing.call();
 
@@ -231,7 +232,7 @@ describe("main functionality", () => {
 
 describe("optional param maxWaitMs", () => {
   it("should support a `maxWait` option", async () => {
-    const mockFn = vi.fn(identity());
+    const mockFn = vi.fn<(x: string) => void>();
     const debouncer = debounce(mockFn, { waitMs: 32, maxWaitMs: 64 });
     debouncer.call("a");
     debouncer.call("b");
@@ -253,8 +254,8 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("should support `maxWait` in a tight loop", async () => {
-    const withMockFn = vi.fn();
-    const withoutMockFn = vi.fn();
+    const withMockFn = vi.fn<() => void>();
+    const withoutMockFn = vi.fn<() => void>();
     const withMaxWait = debounce(withMockFn, { waitMs: 32, maxWaitMs: 128 });
     const withoutMaxWait = debounce(withoutMockFn, { waitMs: 96 });
     const start = Date.now();
@@ -269,7 +270,7 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("should queue a trailing call for subsequent debounced calls after `maxWait`", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debouncer = debounce(mockFn, { waitMs: 200, maxWaitMs: 200 });
     debouncer.call();
     setTimeout(() => {
@@ -287,7 +288,7 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("should cancel `maxDelayed` when `delayed` is invoked", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debouncer = debounce(mockFn, { waitMs: 32, maxWaitMs: 64 });
     debouncer.call();
     await sleep(128);
@@ -301,7 +302,7 @@ describe("optional param maxWaitMs", () => {
   });
 
   it("works like a leaky bucket when only maxWaitMs is set", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<() => void>();
     const debouncer = debounce(mockFn, { maxWaitMs: 32 });
     debouncer.call();
 
@@ -335,8 +336,7 @@ describe("additional functionality", () => {
   });
 
   it("can cancel the timer", async () => {
-    const data = "Hello, World!";
-    const mockFn = vi.fn(() => data);
+    const mockFn = vi.fn<() => string>(constant("Hello, World!"));
     const debouncer = debounce(mockFn, { waitMs: 32 });
 
     expect(debouncer.call()).toBeUndefined();
@@ -355,7 +355,7 @@ describe("additional functionality", () => {
 
     await sleep(32);
 
-    expect(debouncer.call()).toStrictEqual(data);
+    expect(debouncer.call()).toBe("Hello, World!");
     expect(mockFn).toHaveBeenCalledTimes(1);
   });
 
diff --git a/src/funnel.test.ts b/src/funnel.test.ts
index 59e3fe7..21a5674 100644
--- a/src/funnel.test.ts
+++ b/src/funnel.test.ts
@@ -19,7 +19,7 @@ const ARGS_COLLECTOR = (
 
 describe("reducer behavior", () => {
   it("passes the reduced arg to the executor", () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: string) => void>();
     const foo = funnel(mockFn, {
       reducer: constant("hello world"),
       triggerAt: "start",
@@ -32,9 +32,7 @@ describe("reducer behavior", () => {
   });
 
   it("reduces call args", async () => {
-    const mockFn = vi.fn((_total: number): void => {
-      /* do nothing */
-    });
+    const mockFn = vi.fn<(x: number) => void>();
 
     const foo = funnel(mockFn, {
       reducer: (total, item: number) => (total ?? 0) + item,
@@ -55,7 +53,7 @@ describe("reducer behavior", () => {
   });
 
   it("does not invoke if reduceArgs returns undefined", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: unknown) => void>();
     const foo = funnel(mockFn, {
       reducer: constant(undefined),
       triggerAt: "end",
@@ -68,7 +66,7 @@ describe("reducer behavior", () => {
   });
 
   it("supports multiple arguments", () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: unknown) => void>();
     const foo = funnel(mockFn, {
       reducer: (ret: unknown, a: number, b: string, c: boolean) => [
         ret,
@@ -86,7 +84,7 @@ describe("reducer behavior", () => {
   });
 
   test("reducer isn't called again when 'invokedAt: start'", async () => {
-    const mockFn = vi.fn(ARGS_COLLECTOR);
+    const mockFn = vi.fn<typeof ARGS_COLLECTOR>(ARGS_COLLECTOR);
     const foo = funnel(doNothing(), {
       reducer: mockFn,
       triggerAt: "start",
@@ -111,7 +109,7 @@ describe("reducer behavior", () => {
 describe("non-trivial (>0ms) timer duration", () => {
   describe("delay timer", () => {
     test("invokedAt: start", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "start",
@@ -138,7 +136,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("invokedAt: both", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "both",
@@ -169,7 +167,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("invocations in the middle of a window", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "both",
@@ -198,7 +196,7 @@ describe("non-trivial (>0ms) timer duration", () => {
 
   describe("burst timer", () => {
     test("invokedAt: start", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "start",
@@ -225,7 +223,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("invokedAt: both", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "both",
@@ -257,7 +255,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("invokedAt: end", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -287,7 +285,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("invocations in the middle of a window", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -317,7 +315,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("maxBurstDurationMs limits the burst duration", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         minQuietPeriodMs: UT,
@@ -346,7 +344,7 @@ describe("non-trivial (>0ms) timer duration", () => {
 
   describe("both timers", () => {
     test("delay is longer than burst", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         minGapMs: 2 * UT,
@@ -374,7 +372,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("burst is longer than delay", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         minGapMs: UT,
@@ -406,7 +404,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("burst and delay are equal", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         minGapMs: UT,
@@ -435,7 +433,7 @@ describe("non-trivial (>0ms) timer duration", () => {
     });
 
     test("delay and maxBurstDurationMs are equal", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         minGapMs: UT,
@@ -469,7 +467,7 @@ describe("non-trivial (>0ms) timer duration", () => {
 describe("immediate (===0) timer durations", () => {
   describe("delay timer", () => {
     test("invokedAt: start", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "start",
@@ -499,7 +497,7 @@ describe("immediate (===0) timer durations", () => {
     });
 
     test("invokedAt: both", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "both",
@@ -527,7 +525,7 @@ describe("immediate (===0) timer durations", () => {
     });
 
     test("with a non-trivial burst timer", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "both",
@@ -552,7 +550,7 @@ describe("immediate (===0) timer durations", () => {
 
   describe("burst timer", () => {
     test("invokedAt: start", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "start",
@@ -582,7 +580,7 @@ describe("immediate (===0) timer durations", () => {
     });
 
     test("invokedAt: both", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "both",
@@ -614,7 +612,7 @@ describe("immediate (===0) timer durations", () => {
     });
 
     test("invokedAt: end", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -644,7 +642,7 @@ describe("immediate (===0) timer durations", () => {
     });
 
     test("with a non-trivial delay timer", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "both",
@@ -668,7 +666,7 @@ describe("immediate (===0) timer durations", () => {
     });
 
     test("burst timer with non-trivial maxBurstDuration", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         minQuietPeriodMs: 0,
@@ -689,7 +687,7 @@ describe("immediate (===0) timer durations", () => {
   });
 
   test("both timers", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
     const foo = funnel(mockFn, {
       reducer: ARGS_COLLECTOR,
       triggerAt: "both",
@@ -707,7 +705,7 @@ describe("immediate (===0) timer durations", () => {
   });
 
   test("maxBurstDurationMs = 0 limits the burst immediately", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
     const foo = funnel(mockFn, {
       reducer: ARGS_COLLECTOR,
       triggerAt: "end",
@@ -722,7 +720,7 @@ describe("immediate (===0) timer durations", () => {
   });
 
   test("all timeouts zero", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
     const foo = funnel(mockFn, {
       reducer: ARGS_COLLECTOR,
       triggerAt: "both",
@@ -745,7 +743,7 @@ describe("immediate (===0) timer durations", () => {
 
 describe("default minQuietPeriodMs === 0 when minGapMs is not defined", () => {
   test("invokedAt: start", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
     const foo = funnel(mockFn, {
       reducer: ARGS_COLLECTOR,
       triggerAt: "start",
@@ -770,7 +768,7 @@ describe("default minQuietPeriodMs === 0 when minGapMs is not defined", () => {
   });
 
   test("invokedAt: both", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
     const foo = funnel(mockFn, {
       reducer: ARGS_COLLECTOR,
       triggerAt: "both",
@@ -799,7 +797,7 @@ describe("default minQuietPeriodMs === 0 when minGapMs is not defined", () => {
 describe("utility functions", () => {
   describe("flush", () => {
     test("flush triggers an immediate invocation", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -819,7 +817,7 @@ describe("utility functions", () => {
     });
 
     test("flush during active burst", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -844,7 +842,7 @@ describe("utility functions", () => {
 
   describe("cancel", () => {
     test("cancel prevents invocation", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -860,7 +858,7 @@ describe("utility functions", () => {
     });
 
     test("cancel during delay period", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "start",
@@ -880,7 +878,7 @@ describe("utility functions", () => {
 
   describe("isIdle", () => {
     test("isIdle reflects the funnel's state", () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -899,7 +897,7 @@ describe("utility functions", () => {
     });
 
     test("isIdle works when burst duration is 0", async () => {
-      const mockFn = vi.fn();
+      const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
       const foo = funnel(mockFn, {
         reducer: ARGS_COLLECTOR,
         triggerAt: "end",
@@ -923,7 +921,7 @@ describe("utility functions", () => {
 
 describe("edge-cases", () => {
   test("bursts that start late don't prevent delayed invocations", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
     const foo = funnel(mockFn, {
       reducer: ARGS_COLLECTOR,
       triggerAt: "both",
@@ -960,7 +958,7 @@ describe("edge-cases", () => {
   });
 
   test("delay timeouts don't cause an invocation in the middle of bursts", async () => {
-    const mockFn = vi.fn();
+    const mockFn = vi.fn<(x: ReadonlyArray<string>) => void>();
     const foo = funnel(mockFn, {
       reducer: ARGS_COLLECTOR,
       triggerAt: "both",
diff --git a/src/intersection.test.ts b/src/intersection.test.ts
index fda0e25..f8d1bf0 100644
--- a/src/intersection.test.ts
+++ b/src/intersection.test.ts
@@ -50,7 +50,7 @@ it("returns a shallow copy even when all items match", () => {
 
 describe("piping", () => {
   test("lazy", () => {
-    const mock = vi.fn(identity());
+    const mock = vi.fn<(x: number) => number>(identity());
     const result = pipe([1, 2, 3, 4, 5, 6], map(mock), intersection([4, 2]));
 
     expect(mock).toHaveBeenCalledTimes(4);
diff --git a/src/isIncludedIn.test.ts b/src/isIncludedIn.test.ts
index 88b8af2..58a3d43 100644
--- a/src/isIncludedIn.test.ts
+++ b/src/isIncludedIn.test.ts
@@ -125,7 +125,7 @@ describe("legacy v1 replacements", () => {
       });
 
       test("lazy", () => {
-        const count = vi.fn();
+        const count = vi.fn<() => void>();
         const result = pipe(
           [1, 2, 3, 4, 5, 6],
           map((x) => {
diff --git a/src/map.test.ts b/src/map.test.ts
index e327560..ac4e994 100644
--- a/src/map.test.ts
+++ b/src/map.test.ts
@@ -34,7 +34,7 @@ describe("data-last", () => {
 
 describe("pipe", () => {
   it("invoked lazily", () => {
-    const count = vi.fn(multiply(10));
+    const count = vi.fn<(x: number) => number>(multiply(10));
 
     expect(pipe([1, 2, 3], map(count), take(2))).toStrictEqual([10, 20]);
 
@@ -42,7 +42,9 @@ describe("pipe", () => {
   });
 
   it("invoked lazily (indexed)", () => {
-    const count = vi.fn((_: unknown, index: number) => index);
+    const count = vi.fn<(_: unknown, index: number) => number>(
+      (_, index) => index,
+    );
 
     expect(pipe([0, 0, 0], map(count), take(2))).toStrictEqual([0, 1]);
 
diff --git a/src/mapWithFeedback.test.ts b/src/mapWithFeedback.test.ts
index 1b497f9..0e16f4d 100644
--- a/src/mapWithFeedback.test.ts
+++ b/src/mapWithFeedback.test.ts
@@ -40,7 +40,9 @@ describe("data first", () => {
   it("should track index and provide entire items array", () => {
     const data = [1, 2, 3, 4, 5];
 
-    const mockedReducer = vi.fn((acc: number, x: number) => acc + x);
+    const mockedReducer = vi.fn<(acc: number, x: number) => number>(
+      (acc, x) => acc + x,
+    );
 
     mapWithFeedback(data, mockedReducer, 100);
 
@@ -63,7 +65,7 @@ describe("data last", () => {
   });
 
   it("evaluates lazily", () => {
-    const counter = vi.fn((x: number) => x);
+    const counter = vi.fn<(x: number) => number>();
     pipe(
       [1, 2, 3, 4, 5],
       map(counter),
diff --git a/src/omitBy.test.ts b/src/omitBy.test.ts
index 37a1aca..b7cdb1b 100644
--- a/src/omitBy.test.ts
+++ b/src/omitBy.test.ts
@@ -34,7 +34,7 @@ test("symbols are passed through", () => {
 });
 
 test("symbols are not passed to the predicate", () => {
-  const mock = vi.fn();
+  const mock = vi.fn<(x: string) => boolean>();
   const data = { [Symbol("mySymbol")]: 1, a: "hello" };
   omitBy(data, mock);
 
diff --git a/src/once.test.ts b/src/once.test.ts
index d842fab..06f2747 100644
--- a/src/once.test.ts
+++ b/src/once.test.ts
@@ -1,8 +1,9 @@
+import { constant } from "./constant";
 import { once } from "./once";
 
 test("should call only once", () => {
-  const mock = vi.fn(() => ({}));
-  const wrapped = once(mock as () => object);
+  const mock = vi.fn<() => object>(constant({}));
+  const wrapped = once(mock);
   const ret1 = wrapped();
 
   expect(mock).toHaveBeenCalledTimes(1);
diff --git a/src/pickBy.test.ts b/src/pickBy.test.ts
index 170e708..ceb1ff7 100644
--- a/src/pickBy.test.ts
+++ b/src/pickBy.test.ts
@@ -24,7 +24,7 @@ test("symbols are filtered out", () => {
 });
 
 test("symbols are not passed to the predicate", () => {
-  const mock = vi.fn();
+  const mock = vi.fn<(x: string) => boolean>();
   const data = { [Symbol("mySymbol")]: 1, a: "hello" };
   pickBy(data, mock);
 
diff --git a/src/pipe.test.ts b/src/pipe.test.ts
index 2dea535..88b1a87 100644
--- a/src/pipe.test.ts
+++ b/src/pipe.test.ts
@@ -24,7 +24,7 @@ it("should pipe operations", () => {
 
 describe("lazy", () => {
   it("lazy map + take", () => {
-    const count = vi.fn();
+    const count = vi.fn<() => void>();
     const result = pipe(
       [1, 2, 3],
       map((x) => {
@@ -39,7 +39,7 @@ describe("lazy", () => {
   });
 
   it("lazy map + filter + take", () => {
-    const count = vi.fn();
+    const count = vi.fn<() => void>();
     const result = pipe(
       [1, 2, 3, 4, 5],
       map((x) => {
@@ -55,7 +55,7 @@ describe("lazy", () => {
   });
 
   it("lazy after 1st op", () => {
-    const count = vi.fn();
+    const count = vi.fn<() => void>();
     const result = pipe(
       { inner: [1, 2, 3] },
       prop("inner"),
@@ -71,7 +71,7 @@ describe("lazy", () => {
   });
 
   it("break lazy", () => {
-    const count = vi.fn();
+    const count = vi.fn<() => void>();
     const result = pipe(
       [1, 2, 3],
       map((x) => {
@@ -87,7 +87,7 @@ describe("lazy", () => {
   });
 
   it("multiple take", () => {
-    const count = vi.fn();
+    const count = vi.fn<() => void>();
     const result = pipe(
       [1, 2, 3],
       map((x) => {
@@ -103,8 +103,8 @@ describe("lazy", () => {
   });
 
   it("multiple lazy", () => {
-    const count = vi.fn();
-    const count2 = vi.fn();
+    const count = vi.fn<() => void>();
+    const count2 = vi.fn<() => void>();
     const result = pipe(
       [1, 2, 3, 4, 5, 6, 7],
       map((x) => {
diff --git a/src/pullObject.test.ts b/src/pullObject.test.ts
index 0b98e6e..0118224 100644
--- a/src/pullObject.test.ts
+++ b/src/pullObject.test.ts
@@ -73,8 +73,8 @@ describe("dataFirst", () => {
   test("guaranteed to run on each item", () => {
     const data = ["a", "a", "a", "a", "a", "a"];
 
-    const keyFn = vi.fn(identity());
-    const valueFn = vi.fn(identity());
+    const keyFn = vi.fn<(x: string) => string>();
+    const valueFn = vi.fn<(x: string) => string>();
 
     pullObject(data, keyFn, valueFn);
 
@@ -155,8 +155,8 @@ describe("dataLast", () => {
   test("guaranteed to run on each item", () => {
     const data = ["a", "a", "a", "a", "a", "a"];
 
-    const keyFn = vi.fn(identity());
-    const valueFn = vi.fn(identity());
+    const keyFn = vi.fn<(x: string) => string>();
+    const valueFn = vi.fn<(x: string) => string>();
 
     pipe(data, pullObject(keyFn, valueFn));
 
diff --git a/src/take.test.ts b/src/take.test.ts
index 06c8204..dce3cf2 100644
--- a/src/take.test.ts
+++ b/src/take.test.ts
@@ -1,4 +1,3 @@
-import { identity } from "./identity";
 import { map } from "./map";
 import { pipe } from "./pipe";
 import { take } from "./take";
@@ -47,7 +46,7 @@ describe("data last", () => {
   });
 
   test("lazy implementation", () => {
-    const mockFunc = vi.fn(identity());
+    const mockFunc = vi.fn<(x: number) => number>();
     pipe([1, 2, 3, 4, 5], map(mockFunc), take(2));
 
     expect(mockFunc).toHaveBeenCalledTimes(2);
diff --git a/src/tap.test.ts b/src/tap.test.ts
index a0b26bd..0bb3556 100644
--- a/src/tap.test.ts
+++ b/src/tap.test.ts
@@ -8,7 +8,7 @@ const DATA = [1] as const;
 
 describe("data first", () => {
   it("should call function with input value", () => {
-    const fn = vi.fn();
+    const fn = vi.fn<() => void>();
     tap(DATA, fn);
 
     expect(fn).toHaveBeenCalledWith(DATA);
@@ -22,7 +22,7 @@ describe("data first", () => {
 
 describe("data last", () => {
   it("should call function with input value", () => {
-    const fn = vi.fn();
+    const fn = vi.fn<() => void>();
     pipe(DATA, tap(fn));
 
     expect(fn).toHaveBeenCalledWith(DATA);
diff --git a/src/times.test.ts b/src/times.test.ts
index 88b9899..3fee2dd 100644
--- a/src/times.test.ts
+++ b/src/times.test.ts
@@ -25,7 +25,7 @@ describe("data_first", () => {
   });
 
   it("passes idx to fn", () => {
-    const fn = vi.fn();
+    const fn = vi.fn<(x: number) => void>();
     times(5, fn);
 
     expect(fn).toHaveBeenCalledWith(0);
@@ -66,7 +66,7 @@ describe("data_last", () => {
   });
 
   it("passes idx to fn", () => {
-    const fn = vi.fn();
+    const fn = vi.fn<(x: number) => void>();
     pipe(5, times(fn));
 
     expect(fn).toHaveBeenCalledWith(0);
diff --git a/src/zip.test.ts b/src/zip.test.ts
index 2582ebd..6d42b0c 100644
--- a/src/zip.test.ts
+++ b/src/zip.test.ts
@@ -1,5 +1,4 @@
 import { first } from "./first";
-import { identity } from "./identity";
 import { map } from "./map";
 import { pipe } from "./pipe";
 import { zip } from "./zip";
@@ -52,7 +51,7 @@ describe("dataLast", () => {
   });
 
   test("evaluates lazily", () => {
-    const mockFn = vi.fn(identity());
+    const mockFn = vi.fn<(x: number) => number>();
     pipe([1, 2, 3], map(mockFn), zip([4, 5, 6]), first());
 
     expect(mockFn).toHaveBeenCalledTimes(1);
diff --git a/test/lazyInvocationCounter.ts b/test/lazyInvocationCounter.ts
index e82d74e..fef9563 100644
--- a/test/lazyInvocationCounter.ts
+++ b/test/lazyInvocationCounter.ts
@@ -2,7 +2,7 @@ import { map } from "../src/map";
 
 // eslint-disable-next-line @typescript-eslint/explicit-function-return-type, @typescript-eslint/explicit-module-boundary-types
 export const createLazyInvocationCounter = () => {
-  const count = vi.fn();
+  const count = vi.fn<() => void>();
   return {
     count,
     fn: <T>() =>