vitest icon indicating copy to clipboard operation
vitest copied to clipboard

Mocked type requires to mock private properties

Open cexbrayat opened this issue 2 months ago • 2 comments

Describe the bug

Consider the following TS class:

export class MyClass {
  private innerProperty = () => 'inner';

  public outerProperty = () => 'outer';
}

When trying to stub it, Mocked complains if the private property is omitted:

const myClass: Mocked<MyClass> = { outerProperty: vi.fn() };
// Type '{ outerProperty: Mock<() => string>; }' is not assignable to type 'Mocked<MyClass>'.
//  Property 'innerProperty' is missing in type '{ outerProperty: Mock<() => string>; }' but required in type 'MyClass'.

I would expect the mock to be OK if only the public properties are provided.

Reproduction

https://www.typescriptlang.org/play/?#code/JYWwDg9gTgLgBAbzgWQgYwNYFMAmcC+cAZlBCHAOQBuwMWAzjBQNwBQrWAHpLHGgDYBDevRQBPAMJCRiVnDhgowKoLpxgAOw1YoABVJgdMMXAC8cABQBKMwD5Km7VBbt5YAK4AjfsDRwI7nR6BkYm5tZ2lAFBLvjsaBAajHAgktL0AFwo6Ng4ADzIacL09uZI0Tr6EIawYlk0AHREGhH4zEA

System Info

System:
    OS: macOS 15.6.1
    CPU: (10) arm64 Apple M1 Max
    Memory: 9.06 GB / 64.00 GB
    Shell: 5.9 - /bin/zsh
  Binaries:
    Node: 22.18.0 - /Users/cedric/.volta/tools/image/node/22.18.0/bin/node
    Yarn: 1.22.22 - /Users/cedric/.volta/tools/image/yarn/1.22.22/bin/yarn
    npm: 10.9.0 - /Users/cedric/.volta/tools/image/npm/10.9.0/bin/npm
    pnpm: 10.19.0 - /Users/cedric/.volta/tools/image/node/22.18.0/bin/pnpm
  Browsers:
    Chrome: 141.0.7390.123
    Firefox: 142.0
    Safari: 18.6
  npmPackages:
    @vitest/browser-playwright: 4.0.2 => 4.0.2
    @vitest/coverage-v8: 4.0.2 => 4.0.2
    vitest: 4.0.2 => 4.0.2

Used Package Manager

npm

Validations

cexbrayat avatar Oct 25 '25 07:10 cexbrayat

What is your use case? I don't think that's very useful. The Mocked type is exposed because it's a return value of vi.mocked, I wouldn't use it manually.

sheremet-va avatar Nov 27 '25 13:11 sheremet-va

@sheremet-va Thank you for your answer, let me explain the use case.

In Angular/Jasmine testing, you can create a spy object for a service using the jasmine.createSpyObj method. Here's an example of how to create a spy object for a service named UserService:

class UserService {
    private innerField = 'inner';
    getUser() {
        return this.innerField;
    }
}

let userService: jasmine.SpyObj<UserService>;
beforeEach(() => {
  userService = jasmine.createSpyObj('UserService', ['getUser']);
  // use userService in Angular tests
  userService.getUser.and.returnValue('mocked')
});

This works (even if UserService has a private field), as you can see on this TS playground

I was naively thinking that this would translate to Mocked<UserService> in Vitest (the Angular CLI has an automated migration Jasmine -> Vitest that uses MockedObject when replacing SpyObj):

import { MockedObject } from 'vitest';

class UserService {
    private innerField = 'inner';
    getUser() {
        return this.innerField;
    }
}

let userService: MockedObject<UserService>;
beforeEach(() => {
  userService = {
    getUser: vi.fn()
  }
  // ☝️ does not compile as innerField is required by MockedObject
  // use userService in Angular tests
  userService.getUser.mockReturnValue('mocked')
});

This does not compile as you can see in this TS playground

What would be the correct way to type this code?

cexbrayat avatar Nov 28 '25 10:11 cexbrayat

I also tried to find a good replacement for createSpyObj.

What I settled for (for now) is to use vi.fn(class …) and MockedObject<>.

First, instead of createSpyObj<UsersService>('UsersService', ['query']) (in every test file), I created a separate file user-service-mock.ts with the following content:

export const UsersServiceMock = vi.fn(
  class UsersServiceMock implements Partial<UsersService> {
    query = vi.fn().mockResolvedValue([]);
  },
);

The Partial<UsersService> just provides slight code completion, but this is not technically necessary. It is also not necessary to give that class a name, you could also just write vi.fn(class { query = vi.fn(); });. However, this way the UsersServiceMock has a type of Mock<typeof (Anonymous class)>. I figured it might be nice to have a readable class name.

In the test itself, I just replaced

let userService: SpyObj<UsersService>;

with

let userService: MockedObject<UsersService>;

Instead of creating the mocked object instance directly with createSpyObj, I let Angular create it and get the instance with inject and get the correct type with vi.mocked. That is, in the testing module providers, you use the mock class with useClass instead of useValue.

TestBed.configureTestingModule({
  providers: [
    {provide: UsersService, useClass: UsersServiceMock},
  ],
});

userService = vi.mocked(TestBed.inject(UsersService));

PapaNappa avatar Dec 17 '25 09:12 PapaNappa