-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
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
Comments
Can you explain your higher level goal with the use of Vitest's typing utility? For example, why It's hard to suggest without more contexts, but perhaps import { Mocked, Mock, MockedFunction, vi } from "vitest";
const m3: MockedFunction<typeof fn> = vi.fn(fn);
m3.mockClear();
function fn() { } |
|
Thank you, I wasn't aware of 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 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();
});
});
Maybe I'm missing something, but if you look at the definition of vitest/packages/spy/src/index.ts Lines 310 to 317 in 9c552b6
|
Thanks for explaining the motivation and comparison with I tested a few cases below and it looks like there are some cases typescript can still magically infer 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 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);
});
It's using mapped type, so I think it's only testing each property |
Yeah, now that I know about
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 But overall, I think those suggestions can be useful in some cases 👍
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 someMockedFnWithReturn = vi.fn((() => 0) as SomeFnWithReturn);
So it is, I definitely missed that, thanks. I tried putting together a reproduction using simplified versions of Vitest's types to understand why 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) |
Maybe I should close this issue, and open a feature request for a |
|
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>()
} |
|
FWIW the typings shipped with The mock related typings from @ezzatron is pointing not to |
Interesting. I did not realize they were different, we will need to align them then. |
Thank you! Yes, the types from |
@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 |
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, In Jest case, the |
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: 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. 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. |
@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) |
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 👍 |
Describe the bug
I'm trying to use the
Mocked
utility type from thevitest
package, but when I try to invoke.mockClear()
on the result, TypeScript claims that the property does not exist. In contrast, theMock
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
System Info
Used Package Manager
npm
Validations
The text was updated successfully, but these errors were encountered: