Skip to content

fix(jest-mock): distribute spyOn types over overloaded methods - #16191

Open
ahnpnl wants to merge 1 commit into
jestjs:mainfrom
ahnpnl:fix/15998-spyon-overloaded-types
Open

ahnpnl wants to merge 1 commit into
jestjs:mainfrom
ahnpnl:fix/15998-spyon-overloaded-types

Conversation

@ahnpnl

@ahnpnl ahnpnl commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes #15998.

spyOn typings collapsed overloaded methods to the last overload, causing mockReturnValue, mockResolvedValue, mockRejectedValue (and their *Once variants) to reject values from any earlier overload with TS2345: ... is not assignable to parameter of type 'never'.

Minimal repro that previously errored:

function callbackable(): Promise<void>;
function callbackable(cb: (err?: void) => void): void;
function callbackable(cb?: any) { if (!cb) return Promise.resolve(); }

const o = {callbackable};
spyOn(o, 'callbackable').mockRejectedValueOnce(new Error('test')); // TS2345
spyOn(o, 'callbackable').mockResolvedValueOnce(undefined);          // TS2345

The root cause is that the built-in ReturnType<T> (and Parameters<T>) only see the last overload of T. The consumer-side helpers used by mockResolvedValue etc. inherited that limitation.

Fix

Distribute the value types over every overload by extracting all call signatures as a union via a new shared helper FunctionSignatures<F> in @jest/expect-utils. The existing FunctionParameters<F> is now reimplemented on top of it, and jest-mock gains three small internal helpers built on the same primitive:

  • FunctionReturnType<T> — union of every overload's return type
  • FunctionResolveType<T> — distributes ResolveType over every overload
  • FunctionRejectType<T> — distributes RejectType over every overload

These are used by mockReturnValue/Once, mockResolvedValue/Once, mockRejectedValue/Once on MockInstance.

mockImplementation, withImplementation, and mock: MockFunctionState<T> deliberately keep using the original T so user-supplied implementations still have to satisfy all overloads (contravariance preserved).

Supports up to 15 overloads, matching the existing FunctionParametersInternal convention.

Test plan

  • Added packages/jest-mock/__typetests__/overloaded-spyOn.test.ts covering the issue's scenarios (mockRejectedValueOnce, mockResolvedValueOnce, mockReturnValueOnce on an overloaded method).
  • yarn tstyche packages/jest-mock packages/expect-utils — 55 tests / 313 assertions pass.
  • yarn test-types (full suite) — 85 tests / 1580 assertions pass.
  • yarn jest packages/jest-mock packages/expect-utils — 350 tests pass.
  • yarn lint, yarn lint-ts-files, yarn lint:prettier clean on changed files.

@netlify

netlify Bot commented May 18, 2026 •

Copy link
Copy Markdown

✅ Deploy Preview for jestjs ready!

Built without sensitive environment variables

Name Link
🔨 Latest commit c6d0c7f
🔍 Latest deploy log https://app.netlify.com/projects/jestjs/deploys/6abb81609e8dd70008e39a7a
😎 Deploy Preview https://deploy-preview-16191--jestjs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@pkg-pr-new

pkg-pr-new Bot commented May 18, 2026 •

Copy link
Copy Markdown

Open in StackBlitz

babel-jest

npm i https://pkg.pr.new/babel-jest@16191

babel-plugin-jest-hoist

npm i https://pkg.pr.new/babel-plugin-jest-hoist@16191

babel-preset-jest

npm i https://pkg.pr.new/babel-preset-jest@16191

create-jest

npm i https://pkg.pr.new/create-jest@16191

@jest/diff-sequences

npm i https://pkg.pr.new/@jest/diff-sequences@16191

expect

npm i https://pkg.pr.new/expect@16191

@jest/expect-utils

npm i https://pkg.pr.new/@jest/expect-utils@16191

jest

npm i https://pkg.pr.new/jest@16191

jest-changed-files

npm i https://pkg.pr.new/jest-changed-files@16191

jest-circus

npm i https://pkg.pr.new/jest-circus@16191

jest-cli

npm i https://pkg.pr.new/jest-cli@16191

jest-config

npm i https://pkg.pr.new/jest-config@16191

@jest/console

npm i https://pkg.pr.new/@jest/console@16191

@jest/core

npm i https://pkg.pr.new/@jest/core@16191

@jest/create-cache-key-function

npm i https://pkg.pr.new/@jest/create-cache-key-function@16191

jest-diff

npm i https://pkg.pr.new/jest-diff@16191

jest-docblock

npm i https://pkg.pr.new/jest-docblock@16191

jest-each

npm i https://pkg.pr.new/jest-each@16191

@jest/environment

npm i https://pkg.pr.new/@jest/environment@16191

jest-environment-jsdom

npm i https://pkg.pr.new/jest-environment-jsdom@16191

@jest/environment-jsdom-abstract

npm i https://pkg.pr.new/@jest/environment-jsdom-abstract@16191

jest-environment-node

npm i https://pkg.pr.new/jest-environment-node@16191

@jest/expect

npm i https://pkg.pr.new/@jest/expect@16191

@jest/fake-timers

npm i https://pkg.pr.new/@jest/fake-timers@16191

@jest/get-type

npm i https://pkg.pr.new/@jest/get-type@16191

@jest/globals

npm i https://pkg.pr.new/@jest/globals@16191

jest-haste-map

npm i https://pkg.pr.new/jest-haste-map@16191

jest-jasmine2

npm i https://pkg.pr.new/jest-jasmine2@16191

jest-leak-detector

npm i https://pkg.pr.new/jest-leak-detector@16191

jest-matcher-utils

npm i https://pkg.pr.new/jest-matcher-utils@16191

jest-message-util

npm i https://pkg.pr.new/jest-message-util@16191

jest-mock

npm i https://pkg.pr.new/jest-mock@16191

@jest/pattern

npm i https://pkg.pr.new/@jest/pattern@16191

jest-phabricator

npm i https://pkg.pr.new/jest-phabricator@16191

jest-regex-util

npm i https://pkg.pr.new/jest-regex-util@16191

@jest/reporters

npm i https://pkg.pr.new/@jest/reporters@16191

jest-resolve

npm i https://pkg.pr.new/jest-resolve@16191

jest-resolve-dependencies

npm i https://pkg.pr.new/jest-resolve-dependencies@16191

jest-runner

npm i https://pkg.pr.new/jest-runner@16191

jest-runtime

npm i https://pkg.pr.new/jest-runtime@16191

@jest/schemas

npm i https://pkg.pr.new/@jest/schemas@16191

jest-snapshot

npm i https://pkg.pr.new/jest-snapshot@16191

@jest/snapshot-utils

npm i https://pkg.pr.new/@jest/snapshot-utils@16191

@jest/source-map

npm i https://pkg.pr.new/@jest/source-map@16191

@jest/test-result

npm i https://pkg.pr.new/@jest/test-result@16191

@jest/test-sequencer

npm i https://pkg.pr.new/@jest/test-sequencer@16191

@jest/transform

npm i https://pkg.pr.new/@jest/transform@16191

@jest/types

npm i https://pkg.pr.new/@jest/types@16191

jest-util

npm i https://pkg.pr.new/jest-util@16191

jest-validate

npm i https://pkg.pr.new/jest-validate@16191

jest-watcher

npm i https://pkg.pr.new/jest-watcher@16191

jest-worker

npm i https://pkg.pr.new/jest-worker@16191

pretty-format

npm i https://pkg.pr.new/pretty-format@16191

commit: c6d0c7f

@ahnpnl
ahnpnl force-pushed the fix/15998-spyon-overloaded-types branch 2 times, most recently from 0c6bb5c to 5b86080 Compare May 18, 2026 09:41
@ahnpnl
ahnpnl marked this pull request as ready for review May 18, 2026 11:03
Copilot AI review requested due to automatic review settings May 18, 2026 11:03

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR updates Jest mock typings to better handle overloaded function signatures, mainly for spyOn methods whose earlier overloads return promises.

Changes:

  • Adds FunctionSignatures in @jest/expect-utils and reuses it for overload-aware function parameter extraction.
  • Updates jest-mock mock value typings to distribute return/resolve/reject types across overloads.
  • Adds typetests for overloaded spyOn behavior and adjusts existing tests affected by the new implementation typing.

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/jest-mock/src/index.ts Updates SpiedFunction and MockInstance typings for overloaded functions.
packages/jest-mock/__typetests__/overloaded-spyOn.test.ts Adds typetests for overloaded spyOn scenarios.
packages/jest-mock/__typetests__/Mocked.test.ts Updates function-object mock implementation expectations.
packages/jest-haste-map/src/lib/__tests__/walk.test.ts Adjusts a mocked lstat implementation signature.
packages/expect-utils/src/types.ts Adds exported FunctionSignatures and rewires FunctionParameters.
packages/expect-utils/src/index.ts Re-exports the new type helper.
CHANGELOG.md Adds a changelog entry for the typing fix.
Comments suppressed due to low confidence (2)

packages/jest-mock/src/index.ts:206

  • Broadening mockImplementation/withImplementation to accept FunctionImplementation<T> lets callers install a function that no longer satisfies the mocked function type (for example, only one overload, or a function object without its required members). The mock and getMockImplementation() are still typed as T, so downstream code can call other overloads or access members that the stored implementation does not actually support; keep implementations assignable to T (or otherwise align the stored/getter types) and limit the overload distribution to return/resolve/reject values.
  mockImplementation(fn: FunctionImplementation<T>): this;
  mockImplementationOnce(fn: FunctionImplementation<T>): this;
  withImplementation(
    fn: FunctionImplementation<T>,
    callback: () => Promise<unknown>,
  ): Promise<void>;
  withImplementation(fn: FunctionImplementation<T>, callback: () => void): void;

packages/jest-mock/typetests/overloaded-spyOn.test.ts:67

  • This positive test has the same unsoundness for mockImplementationOnce: the one-off implementation will be used for whichever overload is called next, but it is only required to satisfy one overload while the spied function remains typed as supporting all overloads.
  test('mockImplementationOnce accepts a function matching one overload only', () => {
    expect(
      spyOn(o, 'callbackable').mockImplementationOnce(() => Promise.resolve()),
    ).type.toBe<SpiedFunction<typeof o.callbackable>>();

Comment thread CHANGELOG.md Outdated
Comment thread packages/jest-mock/src/index.ts Outdated
Comment thread packages/jest-mock/__typetests__/overloaded-spyOn.test.ts Outdated
Comment thread packages/jest-mock/src/index.ts Outdated
@ahnpnl
ahnpnl force-pushed the fix/15998-spyon-overloaded-types branch from 5b86080 to 4a813f1 Compare May 18, 2026 12:05
@github-actions github-actions Bot added the require-changelog If a PR does requires a changelog entry label Aug 8, 2026

@SimenB SimenB left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#15998 was only ever about mockReturnValue/mockResolvedValue/mockRejectedValue , mockImplementation wasn't broken. Widening it here fixes a real but separate annoyance (namespace members / type predicates blocking plain functions, e.g. Array.isArray), at the cost of the much more common overloaded-callback case.

Would rather split this: keep FunctionReturnType/FunctionResolveType/FunctionRejectType + SpiedFunction<T> = MockInstance<T>, revert mockImplementation/withImplementation back to plain fn: T. Fixes #15998 with none of the downside, and you can drop FunctionSignaturesWithThis/FunctionImplementation and the casts they forced into mockReturnThis/_createMockFunction.

Then Array.isArray-style stripping can be its own PR where it's easier to assess than bundled with other more "safe" fixes 🙂


Also: OverloadedReturnType (tfrom #16237) I don't think is needed with this? ResolveType/RejectType only ever get called with an already-distributed single signature via FunctionSignatures. Worth deleting?

});
});
.mockImplementation(
(p: fs.PathLike, cb: Parameters<typeof origLstat>[1]) => {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a regression?

@ahnpnl ahnpnl Aug 19, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 89a0626. This test is restored to its upstream form, including inferred p. Restoring MockInstance implementation APIs to fn: T exposed that SpiedFunction<T> = MockInstance<T> alone breaks this existing lstat implementation and existing Array.isArray spy implementations.

SpiedFunction now retains only the pre-existing collapsed signatures for mockImplementation, mockImplementationOnce, and withImplementation, while keeping all overloads for the return/resolved/rejected value helpers.

@SimenB would you pls check again 😄

@soltonigiri soltonigiri left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The compatibility overloads on SpiedFunction<T> still lose the distributed
value-helper types when mockImplementation or mockImplementationOnce is
used in a fluent chain.

At 89a0626, this pattern fails with both TypeScript 5.4.5 and 5.9.3:

jest
  .spyOn(target, 'callbackable')
  .mockImplementation(callbackImplementation)
  .mockResolvedValue(undefined);

When the collapsed compatibility overload is selected, mockImplementation
returns that overload's MockInstance<(...args: Parameters<T>) => ReturnType<T>> rather than the outer SpiedFunction<T>. The next helper
therefore sees only the final overload: mockReturnValue(Promise.resolve())
expects void, while the resolved and rejected value types become never.
The same three failures occur after mockImplementationOnce.

The equivalent calls type-check when split across statements, so the behavior
currently depends only on whether the calls are chained. I reproduced these
exact six failures in a 78-assertion matrix covering 1, 2, 4, and 15 overloads;
explicit this, predicate, callback, generic, and rest signatures; callable
objects; and the regular and Once variants of the return-value,
resolved-value, and rejected-value helpers. The remaining 72 assertions pass
at 89a0626.

Could we declare the collapsed compatibility signatures explicitly so that
mockImplementation and mockImplementationOnce return SpiedFunction<T>,
and add a fluent-chain type test? This shape made all 78 assertions pass on
TypeScript 5.9.3 and compiled without diagnostics on TypeScript 5.4.5. The
existing related type tests continue to pass.

@ahnpnl
ahnpnl force-pushed the fix/15998-spyon-overloaded-types branch from 89a0626 to 555e674 Compare September 29, 2026 09:07
@ahnpnl
ahnpnl force-pushed the fix/15998-spyon-overloaded-types branch from 555e674 to c6d0c7f Compare September 29, 2026 09:14

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

require-changelog If a PR does requires a changelog entry

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: (types) spyOn has trouble selecting the correct overload for some methods

4 participants