32.5 Typing Mocked Functions: jest.MockedFunction and vi.Mocked
Alright, let’s talk about mocking in TypeScript. It’s the part of testing where we all collectively agree to pretend things are working so we can test other things in isolation. It’s a necessary lie, and like any good lie, it needs to be well-constructed to be believable. TypeScript, being the wonderfully pedantic friend that it is, will immediately call you out on your flimsy, untyped mocks. That’s where jest.MockedFunction and its Vitest cousin vi.Mocked come in—they’re your tools for building air-tight, convincing lies that keep both your tests and the type checker happy.
The core problem is simple: when you mock a function using Jest’s jest.fn() or Vitest’s vi.fn(), you get a generic mock function. To you, the human, it’s obvious that this mock is standing in for your specific function fetchUserById. But TypeScript just sees jest.Mock (or Mock in Vitest), which is a very generic type. It doesn’t know the shape of the original function—its parameters or its return value. This means trying to mock a return value with the wrong type, or checking if it was called with the wrong arguments, becomes a frustrating game of type assertion (as any) that defeats the entire purpose of using TypeScript.
The Simple, Manual Way: jest.fn(implementation)
Before we reach for the fancy types, let’s acknowledge the straightforward method. If you provide an implementation directly to jest.fn, TypeScript can often infer the types beautifully. This is your first and best option.
// The function we want to mock
function sendEmail(to: string, subject: string, body: string): Promise<void> {
// ... actually sends an email
}
// Mock it with an implementation. TypeScript infers the types!
const mockSendEmail = jest.fn((to: string, subject: string, body: string) => {
console.log(`Would send email to ${to} about ${subject}`);
return Promise.resolve();
});
// This all works perfectly with full type safety.
test('sends an email to the user', async () => {
await userService.notify('user@example.com');
expect(mockSendEmail).toHaveBeenCalledWith(
'user@example.com',
'Welcome!',
expect.any(String)
);
// Try passing a number for 'to' and watch TS yell at you. It's glorious.
});
This is ideal. But life isn’t always ideal. Often you need to configure the mock after its creation (mockReturnValueOnce, mockResolvedValue), and that’s where the inferred type from the initial implementation can sometimes be too narrow. For those cases, and for when you’re mocking modules, we need to be more explicit.
The Power Play: Using jest.MockedFunction
This is the tool for when you need to declare a mock variable first and configure it later. jest.MockedFunction<typeof originalFunction> wraps the original function’s type and applies it to the Jest mock type. It’s a type utility, not a function—you use it to tell TypeScript what your mock should be.
import { getUserPreferences } from './user-api';
// 1. Declare your mock. This is crucial for module mocks.
const mockGetUserPreferences = jest.fn();
// 2. Immediately cast it to the mocked type. This locks it in.
const typedMockGetUserPreferences = mockGetUserPreferences as jest.MockedFunction<
typeof getUserPreferences
>;
// Now, ALL the mock methods are type-safe!
test('handles user preferences', async () => {
// This would cause an error: Argument of type 'number' is not assignable to 'string'
// typedMockGetUserPreferences.mockResolvedValue(42);
// This is correct and beautifully type-safe.
typedMockGetUserPreferences.mockResolvedValue({ theme: 'dark', notifications: true });
await userService.applyPreferences('user123');
expect(typedMockGetUserPreferences).toHaveBeenCalledWith('user123');
});
The Vitest equivalent is vi.Mocked<T> and works exactly the same way. The designers of Vitest were smart enough to not fix what wasn’t broken.
import { mocked } from 'vitest-mock'; // You can also use `vi.mocked()` in newer versions
import { getUserPreferences } from './user-api';
const mockGetUserPreferences = vi.fn();
const typedMock = mockGetUserPreferences as vi.Mocked<typeof getUserPreferences>;
// Proceed with type-safe mocking.
The Pitfall: Mocking Modules and The as Keyword
This is where everyone gets tripped up. When you mock a whole module, you’re replacing real exports with mock exports. You must help TypeScript understand this new, mocked reality. The standard pattern uses the as keyword to cast the entire mocked module to a type where its functions are mocked.
// Assume ./user-api exports: getUserPreferences, setUserPreferences
jest.mock('./user-api');
// This import happens AFTER the mock is set up, so it imports the mocks.
import * as UserApi from './user-api';
// Here's the magic. We tell TS that this imported module is now
// a version where every function is a Jest-mocked function.
const mockedUserApi = UserApi as jest.Mocked<typeof UserApi>;
// Now you can use them with full type safety!
test('module mock works', () => {
mockedUserApi.getUserPreferences.mockReturnValue({ theme: 'light' });
// mockedUserApi.someNonExistentFunction(); // TS will rightfully error here.
userService.loadTheme();
expect(mockedUserApi.getUserPreferences).toHaveBeenCalled();
});
Why Bother? The Three Reasons.
- Catches Bugs in Your Tests: The most common testing bug is “I changed the interface of a real function but forgot to update my mocks.” TypeScript will now scream at you the moment your mock configuration doesn’t match the new signature. It turns a silent test failure (or worse, a false positive) into a compile-time error.
- Enables Autocompletion: Your editor will suggest the correct parameters and return types for
mockResolvedValue,mockImplementation, andtoHaveBeenCalledWith. You stop guessing and start coding. - Documents the Test: Anyone reading your test immediately knows what function this mock is replacing and what its contract is supposed to be. The type is the documentation.
It feels like a bit of extra boilerplate upfront, and it is. But it’s boilerplate that pays for itself the first time it prevents you from shipping a broken test that gave you a green checkmark for all the wrong reasons. It’s the difference between hoping your tests are correct and knowing they are.