Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Property 'mockClear' does not exist on type 'Mocked<...>'. #4723

Open
6 tasks done
ezzatron opened this issue Dec 11, 2023 · 18 comments · May be fixed by #4784
Open
6 tasks done

Property 'mockClear' does not exist on type 'Mocked<...>'. #4723

ezzatron opened this issue Dec 11, 2023 · 18 comments · May be fixed by #4784
Labels
enhancement New feature or request

Comments

@ezzatron
Copy link

Describe the bug

I'm trying to use the Mocked utility type from the vitest package, but when I try to invoke .mockClear() on the result, TypeScript claims that the property does not exist. In contrast, the Mock utility type does not have the same issue.

Reproduction

See https://www.typescriptlang.org/play?ssl=9&ssc=17&pln=1&pc=1#code/JYWwDg9gTgLgBAbzgWQgYwNYFMAmAaFdDAgN2DgF84AzKCEOAIjJiwGcZGBuAKB7QgA7DnBABGAFyFMAHgDaAXVIRgOAHxwAvHDIA6aoIAUBgJS9xukEQDCAGywBDKIbNwA9G7iCIcYGzYArlh8AsLwIABMUqiYuDIwAJ5gWBDUNIIa2noGxoJmPJGWNvZOLlzungAKdMmwCXAA5FaYdo5QDXA4EOxeEPBYAB5+8EJwicmNMdg4Mi5aGiQq6g26fNQBgmgwwKM5JogUQA

import { Mocked, Mock, vi } from "vitest";

const m1: Mock<[], void> = vi.fn(fn);
m1.mockClear(); // no issue

const m2: Mocked<typeof fn> = vi.fn(fn);
m2.mockClear(); // Property 'mockClear' does not exist on type 'Mocked<() => void>'.

function fn() {}

System Info

System:
    OS: macOS 14.1.2
    CPU: (10) arm64 Apple M1 Pro
    Memory: 156.53 MB / 32.00 GB
    Shell: 5.9 - /opt/homebrew/bin/zsh
  Binaries:
    Node: 21.4.0 - /opt/homebrew/bin/node
    Yarn: 1.22.19 - /opt/homebrew/bin/yarn
    npm: 10.2.4 - /opt/homebrew/bin/npm
    pnpm: 8.10.3 - /opt/homebrew/bin/pnpm
  Browsers:
    Chrome: 120.0.6099.71
    Edge: 120.0.2210.61
    Safari: 17.1.2
  npmPackages:
    @vitejs/plugin-react: ^4.2.1 => 4.2.1
    vitest: ^1.0.4 => 1.0.4

Used Package Manager

npm

Validations

@hi-ogawa
Copy link
Contributor

hi-ogawa commented Dec 11, 2023

I'm trying to use the Mocked utility type from the vitest package

Can you explain your higher level goal with the use of Vitest's typing utility? For example, why Mock type is not enough (not convenient) for your use case.

It's hard to suggest without more contexts, but perhaps MockedFunction could be what you're looking for?

import { Mocked, Mock, MockedFunction, vi } from "vitest";

const m3: MockedFunction<typeof fn> = vi.fn(fn);
m3.mockClear();

function fn() { }

https://www.typescriptlang.org/play?#code/JYWwDg9gTgLgBAbzgWQgYwNYFMAmAaFdDA1TXAMQFcA7NGYCaggN2DgF84AzKCEOAESsYWAM4wBAbgBQ0tI3FwQARgBchTAB4A2gF0WEYDgB8cALxxWAOi7UAFLYCUMlVZBEAwgBssAQyh2znAA9MFw1BBwwKKilFiy8tSKIABM6qTYOJowAJ5gWBBc3NSmFta2DtTO0qFKKW6ePv6BkiFhAAq8+bA5cADk7pjeflB9cDgQYuEQ8FgAHtHwjHC5+f0ZuJqB5qbMhiZ9VgkK8CAAzOlEFDR0DNTZeQVFtqWWwDb2Ti5nDUNNAdVpFwbvRlhVHIgOEA

@sheremet-va
Copy link
Member

Mocked is only meant for objects. I don't think it is even documented anywhere. Mocked type has no connection to vi.fn().

@ezzatron
Copy link
Author

It's hard to suggest without more contexts, but perhaps MockedFunction could be what you're looking for?

Thank you, I wasn't aware of MockedFunction. I think it is intended as what I'm looking for, which is something similar to the jest.Mock utility type from @jest/globals. Unfortunately MockedFunction is not quite as nice to use as jest.Mock, but I need to show another example to explain why:

import { vi, Mocked, MockedFunction } from "vitest";
import { jest } from "@jest/globals";

// assume a formal type for a function, e.g. a handler type from a library
type FunctionWithArg = (a: string) => void;

// with another function that accepts the above type as a callback, for example
function accept(fn: FunctionWithArg) {
    fn("x");
}

// we create a typical no-op mock implementation
function fn() {}

// despite not having any args, fn() satisfies the accept() parameter type, with no issues
accept(fn);

/**
 * @jest/globals jest.Mock type
 * 
 * ✅ Just pass the function type as the first type parameter
 * ✅ TypeScript knows about mock functions like mockClear()
 * ✅ Still satisfies the FunctionWithArg type 
 */
const jestMock: jest.Mock<FunctionWithArg> = jest.fn(fn);
jestMock.mockClear();
accept(jestMock);

/**
 * Vitest MockedFunction type
 * 
 * ❌ Type Mock<[], void> is not assignable to type MockedFunction<FunctionWithArg> (args must match exactly)
 * ✅ TypeScript knows about mock functions like mockClear()
 * ✅ Still satisfies the FunctionWithArg type
 */
const vitestMockedFunction: MockedFunction<FunctionWithArg> = vi.fn(fn);
vitestMockedFunction.mockClear();
accept(vitestMockedFunction);

/**
 * Vitest Mocked type
 * 
 * ✅ Just pass the function type as the first type parameter
 * ❌ Property 'mockClear' does not exist on type 'Mocked<FunctionWithArg>'
 * ✅ Still satisfies the FunctionWithArg type
 */
const vitestMocked: Mocked<FunctionWithArg> = vi.fn(fn);
vitestMocked.mockClear();
accept(vitestMocked);

This kind of type utility comes in really handy when you're creating a mock in beforeEach() for use by multiple tests:

import { describe, expect, beforeEach, it, MockedFunction, vi } from "vitest";

// these would be in a library
type SomeFunctionType = (a: string) => void;
const acceptSomeFunction = (fn: SomeFunctionType) => {
  fn("");
};

describe("Example", () => {
  let someFunction: MockedFunction<SomeFunctionType>;

  beforeEach(() => {
    someFunction = vi.fn(() => {}); // at the moment, this is a TS error
  });

  it("tests something", () => {
    acceptSomeFunction(someFunction);
    expect(someFunction).toHaveBeenCalled();
  });

  it("tests something else", () => {
    acceptSomeFunction(someFunction);
    expect(someFunction).toHaveBeenCalled();

    someFunction.mockClear();

    acceptSomeFunction(someFunction);
    expect(someFunction).toHaveBeenCalled();
  });
});

Mocked is only meant for objects. I don't think it is even documented anywhere. Mocked type has no connection to vi.fn().

Maybe I'm missing something, but if you look at the definition of Mocked, it's testing to see if the type argument is a function, and inferring the arg types and return types:

export type Mocked<T> = {
[P in keyof T]: T[P] extends (...args: infer Args) => infer Returns
? MockInstance<Args, Returns>
: T[P] extends Constructable
? MockedClass<T[P]>
: T[P]
} &
T

@hi-ogawa
Copy link
Contributor

Thanks for explaining the motivation and comparison with jest.Mock.
I think overall this issue could be considered as a feature request than jest-compatibility bug?

I tested a few cases below and it looks like there are some cases typescript can still magically infer vi.fn<TArgs, TReturn>. Could these be convenient enough? (These usages are not documented, so probably it's not guaranteed to work forever though...)

Also in my opinion, if you're mocking, then it's likely that you want to lie about type-safety at some point, so I feel there's nothing wrong with using something like vi.fn() as any as a quick escape-hatch.

import { vi, MockedFunction, beforeEach } from 'vitest'

// these would be in a library
type SomeFnType = (a: string) => void;
type SomeFnWithReturn = (a: string) => number;

// in test file
let someMockedFn: MockedFunction<SomeFnType>;
let someMockedFnWithReturn: MockedFunction<SomeFnWithReturn>;

beforeEach(() => {
  // @ts-expect-error
  someMockedFn = vi.fn(() => {});

  // ok (typescript can magically infer generics)
  someMockedFn = vi.fn();

  // ok (need to provide an argument then typscript can infer)
  someMockedFn = vi.fn((_a) => {});


  // @ts-expect-error
  someMockedFnWithReturn = vi.fn(() => {});

  // ok (typescript magic again though return type is lying)
  someMockedFnWithReturn = vi.fn();

  // ok (need to provide an argument and return value to match type)
  someMockedFnWithReturn = vi.fn((_a) => 0);
});

if you look at the definition of Mocked, it's testing to see if the type argument is a function, and inferring the arg types and return types:

It's using mapped type, so I think it's only testing each property T[P] is function instead of T itself.

@ezzatron
Copy link
Author

Thanks for explaining the motivation and comparison with jest.Mock.
I think overall this issue could be considered as a feature request than jest-compatibility bug?

Yeah, now that I know about MockedFunction I'd say it's a feature request.

I tested a few cases below and it looks like there are some cases typescript can still magically infer vi.fn<TArgs, TReturn>. Could these be convenient enough?

At the moment my specific case is one where I need an implementation that returns a specific string, so can't just omit the argument to vi.fn() unfortunately. And the use of underscore-prefixed arguments to mark them as unused depends on having your lint config sorted out. Some of my repos do, some would take some effort to sort that out.

But overall, I think those suggestions can be useful in some cases 👍

Also in my opinion, if you're mocking, then it's likely that you want to lie about type-safety at some point, so I feel there's nothing wrong with using something like vi.fn() as any as a quick escape-hatch.

Also a fair point. It's just a mild sharp edge when coming from Jest, I would say. Most other things get nicer, this stands out as something that doesn't.

I came up with a workaround that's similar, but I prefer to avoid the any, and use as on the argument to vi.fn(), e.g.:

someMockedFnWithReturn = vi.fn((() => 0) as SomeFnWithReturn);

It's using mapped type, so I think it's only testing each property T[P] is function instead of T itself.

So it is, I definitely missed that, thanks.


I tried putting together a reproduction using simplified versions of Vitest's types to understand why MockedFunction has the limitation it does, but unfortunately my toy implementation doesn't share the same problem, so I don't know exactly what the cause is:

type Procedure = (...args: any[]) => any;

type Mock<Args extends any[], Return> = ((...args: Args) => Return) & {
    mockClear: () => void;
};

type MockedFunction<T extends Procedure> = Mock<Parameters<T>, ReturnType<T>>;

function fn<T extends Procedure>(impl?: T): MockedFunction<T> {
    const mock = (...args: any[]) => impl?.(...args);
    mock.mockClear = () => {};

    return mock as MockedFunction<T>;
}

type Handler = (x: string) => "a" | "b";

// implicit impl
const m1: MockedFunction<Handler> = fn();
m1.mockClear(); // no issues
m1("x"); // no issues
m1(); // expected 1 arguments, but got 0 (which is good)

// explicit impl
const m2: MockedFunction<Handler> = fn(() => "a");
m2.mockClear(); // no issues
m2("x"); // no issues
m2(); // expected 1 arguments, but got 0 (which is good)

https://www.typescriptlang.org/play?#code/C4TwDgpgBACgTgewMYQCYFc7QLxQBQB0RAhnAOYDOAXFMQHYgDaAugJRTYB8tDA3AFD9QkKAFlkAawA8AQXIUoEAB7AIdVAvpNmAGigAlCMEx1uuPIRLyacyuy4GjJ9gDIoAb35RvUALaSAYQAbCFIaPHtuADcEAEtUAQBfASFwaHEkCTQAMXQ6JGBYhDopABVFFTUNWEQUDCwzMUkpGFJiXyMIOAoyzj1DYzg6UrTezhSAMzyCorooCZLy5VV1BXhkNEwITjxY3zAggH4aUtYaDKzUXPzC4t6PLx8kYopgP0kOfCICUkoaLRYkSgewOhwIlh+8lYAh870yBH8mWCoTgnwiHG47mSglhWEGc0REloCguOWmt0W434iUEwmgAAl6KgQqjzEoaK84LE6GQgQAiYh8qAAHygfIARnyUgB6aXA-ZBWJIWJvEFBfjPOivPwARnOkjJN1mUkZ6hZjQWEQEvh1CMCIVIVqgsqgdAQwIoFHQEAo-BteD5Sj50OdcrdHq9Pr9OqdLuUkAKaCgOto5HQHTowAoenF6DeZAQbwADPgAO4ACyV5Y9UALCFQrEEcaUByVKvlBw1LzevgATPrMoaZndTcyuha6BZ+YLoX7e3akQ64LGw+7Yp7vb6+wGgyGXeH15Gt72VxUE6pUMnU2R02oszm87XC1AS3gK1Wa3WG0A

@ezzatron
Copy link
Author

Maybe I should close this issue, and open a feature request for a jest.Mock type equivalent instead?

@sheremet-va
Copy link
Member

Maybe I should close this issue, and open a feature request for a jest.Mock type equivalent instead?

Mock is already an equivalent to jest.Mock: https://vitest.dev/guide/migration.html#types

@sheremet-va
Copy link
Member

Maybe I'm missing something, but if you look at the definition of Mocked, it's testing to see if the type argument is a function, and inferring the arg types and return types:

It doesn't check the type argument, it checks type argument properties:

const obj: Mocked<{ foo: (a: string) => void, bar: () => number }> = {
  foo: vi.fn<[string], void>(),
  bar: vi.fn<[], number>()
}

@sheremet-va
Copy link
Member

MockedFunction exists primarily for vi.mocked utility, and should not be reused when you manually write vi.fn. Mock and MockInstance are the only types you need to worry about. Both named exactly the same as they are in jest. The only difference is the argument order.

@sheremet-va sheremet-va closed this as not planned Won't fix, can't repro, duplicate, stale Dec 12, 2023
@mrazauskas
Copy link

mrazauskas commented Dec 12, 2023

FWIW the typings shipped with @types/jest and imported from @jest/globals are different things. The migration guide demonstrates migration from @types/jest only.

The mock related typings from @jest/globals take only one argument (type of the function) and infer all what is needed. Here are the usage details of the Jest built-in types: https://jestjs.io/docs/mock-function-api#typescript-usage

@ezzatron is pointing not to @types/jest, but to @jest/globals. And it seems like the suggestion is to consider reshaping the mock types in similar fashion.

@sheremet-va
Copy link
Member

Interesting. I did not realize they were different, we will need to align them then.

@sheremet-va sheremet-va reopened this Dec 12, 2023
@ezzatron
Copy link
Author

Thank you! Yes, the types from @jest/globals are much nicer to use than the ones from @types/jest, mostly because there's no need to manually specify argument or return types. You just use the type of the actual function being mocked.

@hi-ogawa
Copy link
Contributor

@mrazauskas Thanks for providing the extra context!

It looks like there was a breaking change on Jest at some point jestjs/jest#12489 (Nice, your PR!) and Vitest's Jest migration guide is referring the old version of jest.Mock. Even if it's possible to do similar change on Vitest, it would be also a breaking change, so I'm not sure what's the best strategy to make this move.

@mrazauskas
Copy link

Just to make the context wider: the PR you found belongs to a larger effort to unify/simplify external mocked types and to clean up the internals as well. For instance, Mocked and MaybeMocked can be unified into one type. I mean, using Mocked as the return type of the .mocked() helper reduces duplication of internal types.

In Jest case, the Mocked utility type was always returning deeply mocked object, but the default of the .mocked() helper was shallow mocked. Also bare boolean arguments did not look readable: .mocked(obj, true) vs .mocked(obj, {shallow: true}). The latter is the current implementation.

@mrazauskas
Copy link

By the way, notice that all the changes I was making had extensive type tests. Type inference is powerful, but keep in mind that TypeScript is constantly improving it. This means that not all users might be able to use the typings. The lowest supported TypeScript version must be set. And here is the problem: how to test that?

The short answer: tstyche --target 4.8,5.0,latest.

With all the changes I made in Jest repo, I have contributed ca. 1000 type test assertions. As usually: solving a problem brings you to another problem. In this case, type testing at scale was rather an issue. I was hitting lots of limitations, one of them was running tests on specified TypeScript version. To address all that, I have build TSTyche, a type testing tool for TypeScript. Documentation is here: https://tstyche.org

Of course, I did consider other type testing libraries around. expect-type was one of them. Its expect style API is great and expect-type is a nice tool for small projects. But it does not scale.

I was already mentioning TSTyche in other issues. Now you can see it from another perspective.

@sheremet-va
Copy link
Member

I was already mentioning TSTyche in other issues. Now you can see it from another perspective.

I don't see how this is relevant to this issue though. Let's keep the discussion clean and to the point without promoting other tools.

@hi-ogawa hi-ogawa linked a pull request Dec 20, 2023 that will close this issue
11 tasks
@hi-ogawa
Copy link
Contributor

@ezzatron I was wondering if you've ever considered writing jest-compatible fn/mock wrapper on your own. I think something like this might get close enough for the time being:

import { Mock, vi } from "vitest";

// user-land jest-compatible wrapper
type Procedure = (...args: any[]) => any;
type UnknownProcedure = (...args: unknown[]) => unknown;
type JestCompatMock<T extends Procedure = UnknownProcedure> = Mock<Parameters<T>, ReturnType<T>>;

function jestCompatFn<T extends Procedure = UnknownProcedure>(): JestCompatMock<T>
function jestCompatFn<T extends Procedure = UnknownProcedure>(implementation: T): JestCompatMock<T>
function jestCompatFn<T extends Procedure = UnknownProcedure>(implementation?: T): JestCompatMock<T> {
  return (vi.fn as any)(implementation);
}

I tested this on your example from #4723 (comment)

@ezzatron
Copy link
Author

Thanks @hi-ogawa, I'm sure I can make use of that.

In the longer term, it would be really nice to have this style of mock typing out of the box in Vitest, as I have many different repos with Jest tests that I'd like to port. But it's definitely not something I would consider a major roadblock 👍

@sheremet-va sheremet-va added enhancement New feature or request and removed pending triage labels Dec 28, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
enhancement New feature or request
Projects
Status: No status
4 participants