Definitelytyped: [@types/jest] Wrong type for jest. spyOn

Created on 3 Feb 2020  路  10Comments  路  Source: DefinitelyTyped/DefinitelyTyped

  • Authors: @guidokessels @CertainPerformance @devanshj @Favna

Edit
See the next comment (https://github.com/DefinitelyTyped/DefinitelyTyped/issues/42067#issuecomment-581483243)


Currently we need to do an implicit cast when mocking a module:

jest.mock('../src/date-pick/get-hours-sao-paulo')
import getHoursSaoPaulo from '../src/date-pick/get-hours-sao-paulo'
const mockedGetHoursSaoPaulo = getHoursSaoPaulo as unknown as jest.Mock<typeof getHoursSaoPaulo>

So when we want to use a function such as mockImplementation we should to write:

mockedGetHoursSaoPaulo.mockImplementation(() => 21)

But for some type definition problem TS says a false-positive error:
image

saying that we should to write

mockedGetHoursSaoPaulo.mockImplementation(() => () => 21)

but it's wrong!

Maybe we have more of this problem on others functions on jest.Mock.

All 10 comments

I noticed another bug:

image

But is okay to use others types than string in this case.

@macabeus If you look at the type definition of Mock it's this

https://github.com/DefinitelyTyped/DefinitelyTyped/blob/c56c683e97a4bc7a9c056a601b19d85b604695ed/types/jest/index.d.ts#L1037-L1040

Notice the type parameter doesn't take the function type but the return type and arguments so your code should be

jest.Mock<ReturnType<typeof getHoursSaoPaulo>, Arguments<typeof getHoursSaoPaulo>>

// Even better make a type alias:
type MockFromFunction<F extends (...args: any[]) => any>
    = jest.Mock<ReturnType<F>, Arguments<F>>

Away from keyboard so haven't tested but most probably should work

@devanshj Thank you for fast reply.
It's almost worked, I just needed to replace Arguments to Parameters, so now it's working:

const mockedGetHoursSaoPaulo = getHoursSaoPaulo as jest.Mock<ReturnType<typeof getHoursSaoPaulo>, Parameters<typeof getHoursSaoPaulo>>

mockedGetHoursSaoPaulo.mockImplementation(() => 21)

But it's a messy code... maybe we could replace to work with just typeof getHoursSaoPaulo?


Also, there is this another problem.

Thank you for fast reply.

No problems!

I just needed to replace Arguments to Parameters

Haha yes forgot it's Parameters

But it's a messy code... maybe we could replace to work with just typeof getHoursSaoPaulo

Well yeah the type parameters are a little unintuitive (maybe there might be a good reason for that) but having a breaking change just to fix this would be too much. As I suggested to make a type alias that would be enough I guess.

Also, there is this another problem.

I'll check it out once I'm free :)

I noticed another bug:

image

But is okay to use others types than string in this case.

@macabeus So I looked into this the problem is spyOn's type doesn't cover the case where the the function passed is a constructor. The reason it still allows "Date" as second parameter and the reason you are told to return string in the mock implementation because Date() (notice no new) returns a string. The type also reflects that.

So this is indeed a bug. I'm sending a PR in a sec. Also it would be great if you rename the issue to be bug in spyOn also link the description to the comment that describes it.

Okay. I did it. Very thank you to improve this package =]

I'm also trying to accomplish the same thing as @devanshj pointed out here but I'm still running into the same error. Wondering if anyone had any ideas?

I'm doing

const mockedDate: Date = new Date(2020, 0);
jest.spyOn(global, 'Date')
            .mockImplementationOnce(() => mockedDate);

and getting TS2322: Type 'Date' is not assignable to type 'string'. on the mockedDate.

For now I can get around it by doing (): any => mockedDate but would prefer not to if at all possible!

Okay so there are too many problems at play.

  1. The reason for the error is that Date is also a function so spyOn picks the function overload (3rd one) instead of the class/constructor overload (4th one). Hence the mock implementation asks for the function overload of Date that returns a string instead of Date.

    Now one way to "fix" this by simply making typescript ignore the function overload Date like this

    const mockedDate: Date = new Date(2020, 0);
    
    type PatchedGlobal = {
        Date: new (...args: ConstructorParameters<DateConstructor>) => Date
    }
    jest.spyOn(global as PatchedGlobal, "Date")
    .mockImplementationOnce(() => mockedDate);
    
  2. Another problem is that the mocked implementation is actually kinda wrong as Date() should return string but yours would return Date. So another fix would be something like this:

    function _MockedDate(this: any) {
        if (!(this instanceof _MockedDate)) return new Date(2020, 0).toString();
        return (Date as any).call(this, 2020, 0)
    }
    _MockedDate.prototype = Object.create(Date.prototype)
    _MockedDate.prototype.constructor = _MockedDate
    _MockedDate.__proto__ = Date
    const MockedDate = _MockedDate as any as DateConstructor;
    
    test("MockedDate", () => {
        expect(typeof MockedDate()).toBe("string")
        expect(new MockedDate() instanceof MockedDate).toBe(true)
        expect(new MockedDate() instanceof Date).toBe(true)
        expect(new MockedDate().toString()).toBe(new Date(2020, 0).toString()) // throws (before even failing) check the update
    })
    
    jest.spyOn(global, "Date")
    .mockImplementationOnce(MockedDate);
    

    UPDATE: THIS DOESN'T WORK, tbh I have no idea how to make a class that is also a function apparently inheriting native class is hella work as compared to inheriting userland classes smh

  3. The types of spyOn itself are wrong as they don't take into consideration all the overloads of the spied function and classes. This problem would be in all codebases that use Parameters or ReturnType because they take into account only the first overload. So the most idealist scenario is mockImplementationOnce to have a type that enforces to implement all overloads (I'll take a look on how to make this work maybe later sometime)

  4. So the problem is with spyOn itself but even if it had the ideal types MockedDate would still require a lot of anys because how in the world you can have a type like { (): string, new (): Date }, the fact that Date can be called without new and returns a string itself is weird.

So to conclude

  1. On your end I would suggest go with the second solution because it takes care of the case where the code being tested has Date() (no new) somewhere. If you're sure the code being tested always has new Date() simply go with mockImplementationOnce(() => mockedDate as any) no need of any (no pun intended xD) ceremony.
  2. On jest's end we need to change the type so it addresses the 3rd problem.

Okay so there are too many problems at play.

  1. The reason for the error is that Date is also a function so spyOn picks the function overload (3rd one) instead of the class/constructor overload (4th one). Hence the mock implementation asks for the function overload of Date that returns a string instead of Date.
    ...

@devanshj This is a really good explanation - thanks! I spent a good while trying to figure out where the string was coming from 馃槀
Understand now this is by no means a simple fix and one of the many interesting things of Javascript haha but really appreciate you sending across them workarounds for this issue!

@devanshj This is a really good explanation - thanks! I spent a good while trying to figure out where the string was coming from 馃槀
Understand now this is by no means a simple fix and one of the many interesting things of Javascript haha but really appreciate you sending across them workarounds for this issue!

Haha you're welcome! It indeed has no simple fix.

Was this page helpful?
0 / 5 - 0 ratings