Skip to main content
Back to Blog
Reference
2026-09-28

vi.mocked in Vitest: Type-Safe Mocks in TypeScript

vi.mocked guide for Vitest TypeScript tests: type module mocks, deep mocks, partial returns, and fix mockReturnValue errors without unsafe casts.

vi.mocked in Vitest: Type-Safe Mocks in TypeScript

vi.mocked is Vitest's TypeScript helper for telling the compiler that an imported function, module object, or nested property is already mocked. It does not mock anything by itself. It returns the same object at runtime, with mock-aware types layered on top. Use it after vi.mock, vi.spyOn, vi.importMock, or another setup step has actually created the mock.

Version context: Vitest 5.0.0 shipped on September 3, 2026, and 5.0.2 is the current release. The vi.mocked overloads described here match the current docs at vitest.dev. If your project is still on Vitest 4, its docs remain available at v4.vitest.dev, and the helper behaves the same way for everything shown in this guide.

For QA engineers using AI coding agents, vi.mocked is a guardrail against a common failure mode: the agent correctly mocks a module, then TypeScript rejects mockResolvedValue, mockReturnValue, or nested mock access because the import still has its original production type. The broader Vitest mocking vi.mock complete guide explains module mocking strategy. The Vitest mock hoisting reference error fix is the companion when the problem is execution order rather than typing.

The Small But Important Contract

The official API says vi.mocked(object) is a type helper. That sentence should drive every design decision. If a value is not a mock at runtime, vi.mocked will not make it one. If you call vi.mocked(realFunction).mockReturnValue(...) without mocking or spying first, TypeScript may be satisfied while runtime still fails because the real function has no mock methods.

The current overloads support a boolean deep argument or an options object with partial and deep. partial: true loosens return values to Partial<T>. deep: true tells TypeScript that nested functions or nested return shapes are mocked too. partial: true, deep: true combines both for recursive partial typing.

FormType meaningTypical use
vi.mocked(fn)Treat one function as a mocked functionImported function mocked by vi.mock
vi.mocked(module)Treat first-level function exports as mockedNamespace import from a mocked module
vi.mocked(module, { deep: true })Treat nested properties as mockedSDK objects with nested clients
vi.mocked(fn, { partial: true })Allow partial resolved or returned valuesFetch wrappers returning large shapes
vi.mocked(obj, { partial: true, deep: true })Allow nested partial mocked shapesComplex service clients in unit tests
import { describe, expect, it, vi } from 'vitest';
import { getAccount } from './account-service';

vi.mock('./account-service', () => ({
  getAccount: vi.fn(),
}));

describe('account summary', () => {
  it('uses a typed mocked function', async () => {
    vi.mocked(getAccount).mockResolvedValue({
      id: 'acct_123',
      status: 'active',
    });

    await expect(getAccount('acct_123')).resolves.toEqual({
      id: 'acct_123',
      status: 'active',
    });
    expect(getAccount).toHaveBeenCalledWith('acct_123');
  });
});

That example is intentionally plain. The mock happens in vi.mock. The type refinement happens in vi.mocked. The assertion checks both the returned value and the side effect of calling the dependency with the intended ID.

The Production Module Shape Matters

Type-safe mocks are only useful when the real module is typed well. If production exports use any, the mock cannot recover meaningful guarantees. A clean pattern is to type the service function at its source, import it normally in production code, and mock it in tests with the same import path.

export type Account = {
  id: string;
  status: 'active' | 'suspended';
};

export async function getAccount(id: string): Promise<Account> {
  const response = await fetch('/api/accounts/' + encodeURIComponent(id));

  if (!response.ok) {
    throw new Error('Account request failed');
  }

  return response.json() as Promise<Account>;
}
import { getAccount } from './account-service';

export async function formatAccountBadge(id: string): Promise<string> {
  const account = await getAccount(id);
  return account.status === 'active' ? 'Account active' : 'Account suspended';
}

Now the test can mock exactly the dependency contract and assert the user-facing behavior. The mock return must include a valid status, and TypeScript will reject accidental values such as 'enabled'.

import { describe, expect, it, vi } from 'vitest';
import { getAccount } from './account-service';
import { formatAccountBadge } from './format-account-badge';

vi.mock('./account-service', () => ({
  getAccount: vi.fn(),
}));

describe('formatAccountBadge', () => {
  it('formats active accounts', async () => {
    vi.mocked(getAccount).mockResolvedValue({
      id: 'acct_123',
      status: 'active',
    });

    await expect(formatAccountBadge('acct_123')).resolves.toBe('Account active');
    expect(getAccount).toHaveBeenCalledTimes(1);
    expect(getAccount).toHaveBeenCalledWith('acct_123');
  });
});

What people get wrong is reaching for as unknown as Mock or as any when TypeScript complains. That silences the compiler precisely where you want it to protect you: return shapes, parameter lists, and async behavior. vi.mocked keeps the connection to the original function signature.

vi.mocked Versus vi.fn, vi.mock, vi.spyOn, And Types

vi.fn creates a mock function. vi.mock replaces a module before imports run. vi.spyOn wraps an existing object method. vi.mocked changes TypeScript's view of a value that has already been mocked. The related exported types such as MockedFunction and Mocked are useful for variables and helper functions, but vi.mocked is usually cleaner at the call site.

ToolRuntime effectType effectUse when
vi.fn()Creates a standalone mock functionReturns a typed mockYou inject a callback or fake dependency
vi.mock()Mocks an imported moduleModule import may still look originalYou replace module exports for a test file
vi.spyOn()Wraps an object methodReturns a spy mockYou observe or override one method
vi.mocked()No runtime changeNarrows to mocked typesTypeScript cannot see the mock methods
MockedFunction<T>No runtime changeNames a mocked function typeYou store mocks in variables or helpers
import { describe, expect, it, vi } from 'vitest';
import type { MockedFunction } from 'vitest';

type LookupUser = (id: string) => Promise<{ id: string; email: string }>;

const lookupUser: MockedFunction<LookupUser> = vi.fn<LookupUser>();

describe('typed standalone mock', () => {
  it('keeps parameter and return types', async () => {
    lookupUser.mockResolvedValue({
      id: 'user_1',
      email: 'qa@example.test',
    });

    await expect(lookupUser('user_1')).resolves.toEqual({
      id: 'user_1',
      email: 'qa@example.test',
    });
    expect(lookupUser).toHaveBeenCalledWith('user_1');
  });
});

Use the type aliases when creating a mock variable yourself. Use vi.mocked when the value came from an import or object you do not want to restate. Both approaches are valid, but mixing them randomly tends to confuse AI-generated tests.

Deep Mocks For SDK-Style Objects

Many frontend and API clients expose nested objects, such as client.users.get or client.billing.invoices.create. If vi.mock replaces those nested functions, plain vi.mocked(client) only tells TypeScript about the first level by default. Use { deep: true } when the nested object really is mocked at runtime.

export const billingClient = {
  invoices: {
    async create(input: { accountId: string; amountCents: number }) {
      return {
        id: 'inv_live',
        accountId: input.accountId,
        amountCents: input.amountCents,
        status: 'open' as const,
      };
    },
  },
};
import { describe, expect, it, vi } from 'vitest';
import { billingClient } from './billing-client';

vi.mock('./billing-client', () => ({
  billingClient: {
    invoices: {
      create: vi.fn(),
    },
  },
}));

describe('billing client mock', () => {
  it('types nested invoice creation', async () => {
    vi.mocked(billingClient, { deep: true }).invoices.create.mockResolvedValue({
      id: 'inv_test',
      accountId: 'acct_123',
      amountCents: 5000,
      status: 'open',
    });

    await expect(
      billingClient.invoices.create({ accountId: 'acct_123', amountCents: 5000 }),
    ).resolves.toMatchObject({
      id: 'inv_test',
      status: 'open',
    });
  });
});

Deep typing should match deep runtime mocking. If you only spy on billingClient.invoices.create, do not pretend every nested property is mocked. Overusing deep: true makes tests look safer than they are.

Partial Returns Without Lying About The Function

partial: true is useful when the production return type is large but a test only needs a few fields. The official docs show partial behavior for async and nested values. The key is to reserve it for functions where the code under test genuinely reads a subset. If production later reads a new field, a partial mock can hide the missing data unless your assertion exercises that branch.

Return styleUse partial?Why
Small domain object with three fieldsUsually noFull values are clearer and safer
Fetch Response objectOften yesTests may only care about ok or status
SDK object with many optional fieldsSometimesKeep test focused on fields consumed by code
Public contract you ownPrefer noFull mock detects accidental contract drift
Deep nested SDK shapepartial plus deep only when neededAvoid fabricating huge nested fixtures
import { describe, expect, it, vi } from 'vitest';
import { fetchProfile } from './profile-api';

vi.mock('./profile-api', () => ({
  fetchProfile: vi.fn(),
}));

describe('profile loader', () => {
  it('allows a partial response when only ok is consumed', async () => {
    vi.mocked(fetchProfile, { partial: true }).mockResolvedValue({
      ok: false,
      status: 503,
    });

    const response = await fetchProfile('user_1');

    expect(response.ok).toBe(false);
    expect(response.status).toBe(503);
  });
});

That sample is appropriate only if the tested branch reads ok and status. If the code later reads headers, json(), or url, the test should grow a more complete fake or use a real Response.

Hoisting Still Controls When The Mock Exists

vi.mocked solves TypeScript errors. It does not solve hoisting errors. Vitest hoists vi.mock, vi.unmock, and vi.hoisted to the top of the file. The current migration docs say Vitest 5 throws when hoisted mock calls are placed inside functions, blocks, or test callbacks. Use top-level vi.mock for static module replacement. Use vi.doMock with dynamic imports when you intentionally need a per-test mock.

import { describe, expect, it, vi } from 'vitest';

const mocks = vi.hoisted(() => ({
  readFeatureFlag: vi.fn<(name: string) => boolean>(),
}));

vi.mock('./feature-flags', () => ({
  readFeatureFlag: mocks.readFeatureFlag,
}));

describe('hoisted module mock', () => {
  it('uses a hoisted mock safely', async () => {
    mocks.readFeatureFlag.mockImplementation((name) => name === 'checkout-v2');

    const { isCheckoutV2Enabled } = await import('./checkout-mode');

    expect(isCheckoutV2Enabled()).toBe(true);
    expect(mocks.readFeatureFlag).toHaveBeenCalledWith('checkout-v2');
  });
});

The diagnostic clue is different. If TypeScript says Property mockResolvedValue does not exist, vi.mocked is probably relevant. If Vitest says a mock factory referenced a top-level variable, or that a hoisted call was defined outside top-level scope, the fix is vi.hoisted, moving the mock, or changing to vi.doMock plus dynamic import.

Failure Mode: The Test Compiles But The Mock Is Real

A realistic failure starts with an agent adding this line to a test: vi.mocked(sendEmail).mockResolvedValue({ id: 'email_1' }). TypeScript passes. At runtime, Vitest throws mockResolvedValue is not a function. The missing step is that sendEmail was never mocked. The helper changed the type, not the object.

Diagnose by asking three questions. Was sendEmail imported with ESM import, not require? Is there a top-level vi.mock('./email-service') or a vi.spyOn before the method is configured? Does the mock path exactly match the import path used by the code under test? Path aliases and setup files often create the mismatch.

Error or symptomLikely causeFix
mockResolvedValue is not a functionValue was never mocked at runtimeAdd vi.mock, vi.spyOn, or vi.fn
TypeScript rejects mockReturnValueImport still has original typeWrap the import with vi.mocked
Mock factory cannot read variablevi.mock hoistingUse vi.hoisted or define inside factory
Mock does not affect code under testImport path mismatch or setup cacheMatch paths and avoid importing mocked modules in setup
Partial mock hides missing fieldpartial: true overusedReturn a full domain object for owned contracts
import { describe, expect, it, vi } from 'vitest';
import * as emailService from './email-service';

describe('email sender', () => {
  it('spies before using vi.mocked', async () => {
    const spy = vi
      .spyOn(emailService, 'sendEmail')
      .mockResolvedValue({ id: 'email_1', accepted: true });

    vi.mocked(emailService.sendEmail).mockResolvedValueOnce({
      id: 'email_2',
      accepted: true,
    });

    await expect(emailService.sendEmail('qa@example.test')).resolves.toEqual({
      id: 'email_2',
      accepted: true,
    });
    expect(spy).toHaveBeenCalledWith('qa@example.test');
  });
});

The spy creates the runtime mock. vi.mocked then gives the imported method mock-aware types. This is often the least disruptive fix for legacy modules where a full vi.mock factory would be too broad.

Running Focused Mock Tests In CI

Vitest uses -t or --testNamePattern for test name filtering, and current docs note that the pattern matches the full name. In Vitest 5, suite and test name segments are joined with >; before Vitest 5, the join format mirrored Jest with spaces. That difference matters for very specific filters.

{
  "scripts": {
    "test": "vitest run",
    "test:watch": "vitest",
    "test:mocks": "vitest run -t mocked",
    "test:account": "vitest run --testNamePattern 'account > formats'"
  }
}
name: vitest

on:
  pull_request:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 22
          cache: npm
      - run: npm ci
      - run: npm run test -- --reporter=default

Avoid building CI around type casts. A failing TypeScript mock is often an early signal that the production contract changed. Let tsc --noEmit or Vitest's transform step catch it before a UI test spends minutes exercising a branch fed by an impossible fixture.

A Decision Checklist For Agents And Humans

When you see a mock typing error, choose the smallest tool that matches the runtime setup. If the mock is a standalone function you create in the test, use vi.fn with a function type or MockedFunction. If the mock is an imported module function replaced by vi.mock, use vi.mocked(importedFunction). If it is a nested SDK client, confirm the mock factory creates nested vi.fn methods before using { deep: true }. If you only need a subset of a huge returned shape, consider partial: true, then assert the branch that reads those fields.

Keep assertions meaningful. For async mocks, await the code under test and assert both the resulting behavior and the dependency call. For string outputs, use anchored regular expressions when matching formats, such as /^acct_[a-z0-9]+$/ in rendered code. For side effects, check the call arguments, persisted state, emitted event, or visible UI change. A mock configured without an assertion is only a test arrangement, not proof.

Frequently Asked Questions

Does vi.mocked create a mock at runtime?

No. vi.mocked returns the object you pass to it and changes only TypeScript's understanding of that object. You still need vi.mock, vi.fn, vi.spyOn, or vi.importMock to create the runtime mock. If a test throws mockReturnValue is not a function, the object probably was real at runtime even though the compiler accepted the call.

When should I use deep true with vi.mocked?

Use { deep: true } when nested properties are actually mocked, such as an SDK object whose factory returns { invoices: { create: vi.fn() } }. Do not use it as a blanket fix for every object. Deep mocked typing can make nested methods appear configurable even when your runtime factory did not replace them. Match the type helper to the real mock shape.

Is partial true safe for domain objects?

It is safe only when the code under test reads a true subset of a large shape and the assertion covers that branch. For domain objects you own, full objects are usually better because they catch contract drift. partial: true is most helpful for platform or SDK objects where building a complete fake would distract from the behavior being tested.

Why does vi.mocked not fix hoisting errors?

Hoisting is about when Vitest executes module mock calls. Typing is about what TypeScript believes after the mock exists. vi.mocked only helps the second problem. If the error mentions top-level variables in a mock factory or nested vi.mock calls, move the mock to top level, use vi.hoisted, or switch to vi.doMock with dynamic import for test-specific setup.