Definitelytyped: [@types/jest] spyOn Argument is not assignable to parameter of type

Created on 18 Feb 2019  路  9Comments  路  Source: DefinitelyTyped/DefinitelyTyped

  • [x] I tried using the @types/jest package and had problems. (Version 24.0.6)
  • [x] I tried using the latest stable version of tsc. https://www.npmjs.com/package/typescript (Version 3.3.3)
  • [x] I have a question that is inappropriate for StackOverflow. (Please ask any appropriate questions there).

Since @types/[email protected].* has been released I've been running into a typing issue where spyOn can't seem to pick up the correct property out of the object that's being spied on. Here is a short piece of sample code:

export interface PopoverProps {
  wrapperElementProps?: any;
  onOpenChange?: (isOpen: boolean) => void;
}

let spy: jest.SpyInstance<void, [boolean]>;
let defaultProps: PopoverProps = {
  onOpenChange: () => {}
};
spy = jest.spyOn(defaultProps, "onOpenChange");

Which results in the following error:

    src/popover2.test.tsx:10:32 - error TS2345: Argument of type '"onOpenChange"' is not assignable to parameter of type '"wrapperElementProps"'.

    10 spy = jest.spyOn(defaultProps, "onOpenChange");
                                      ~~~~~~~~~~~~~~

I think the issue is around type ArgsType<T> = T extends (...args: infer A) => any ? A : never; but that just maybe me projecting my lack of knowledge around how infer actually works.

  • @NoHomey
  • @jwbay
  • @asvetliakov
  • @alexjoverm
  • @epicallan
  • @ikatyang
  • @wsmd
  • @JamieMason
  • @douglasduteil
  • @ahnpnl
  • @JoshuaKGoldberg
  • @UselessPickles
  • @r3nya
  • @Hotell
  • @sebald
  • @andys8
  • @antoinebrault

Most helpful comment

This seems to work. I'll add more tests.

function spyOn<T extends {}, Y extends Required<T>, M extends FunctionPropertyNames<Y>>(object: T, method: M): Y[M] extends (...args: any[]) => any ? SpyInstance<ReturnType<Y[M]>, ArgsType<Y[M]>> : never;

All 9 comments

why can't you just use?

let defaultProps: PopoverProps = {
  onOpenChange: jest.fn(),
};

In 23, spyOn would return a typed version of the method being mocked where fn() returns merely a mock of unless you explicitly type it which is why I've preferred spyOn so far (as it determined the types for you)

Well, what about this? (if i understood your type needs correctly):

let defaultProps: jest.Mocked<PopoverProps> = {
  onOpenChange: jest.fn(),
};
defaultProps.onOpenChange.mock // typed mock, i.e. jest.MockInstance<void, [boolean]>

I don't disagree that there are ways to work around the issue I think the purpose of me opening this bug is to determine why the specific typing of spyOn seems to have a typing failure. If instead the outcome you're alluding to is to drop support for spyOn in favor for jest.fn() then maybe the option here is to remove spyOn from the types?

I'll look at it. Seems weird to me that it's working with objects but not interfaces. Maybe it's a TypeScript bug.

Digging into this it seems the issue is that the type is optional which is causing the "extends" functionality of which determines which spyOn to use to not work. What is weird about this specific case is if you go through a generic (e.g. ArgsType) it works successfully but attempting to pick out the types directly doesn't. For example:

      type prop1 = (isOpen: boolean) => void;
      type argsInfered1 = prop1 extends (...args: infer A) => any ? A : never;
      // argsInfered1 = [boolean] - Correct

      type prop2 = undefined | ((isOpen: boolean) => void);
      type argsInfered2 = prop2 extends (...args: infer A) => any ? A : never;
      // argsInfered2 = never - Incorrect

      type ArgsType<T> = T extends (...args: infer A) => any ? A : never;
      let argsInferredDirectly1: ArgsType<prop1>;
      // argsInferredDirectly1 = [boolean] - Matches argsInfered1

      let argsInferredDirectly2: ArgsType<prop2>;
      // argsInferredDirectly2 = [boolean] - Shouldn't this match argsInfered2?

Hopefully this helps when investigating... Fairly sure I'm at the length of my typescript knowledge now :-)

The optional field makes the conditional on array bug.

function test<T extends {}, M extends keyof T>(object: T, method: M): T[M] extends (...args: any) => any ? T[M] : never;

export interface PopoverProps {
  wrapperElementProps?: any;
  onOpenChange?: (isOpen: boolean) => void;
}
let defaultProps: PopoverProps = {
  onOpenChange: () => {}
};
test(defaultProps, 'onOpenChange') // returns never instead of (isOpen: boolean) => void

This seems to work. I'll add more tests.

function spyOn<T extends {}, Y extends Required<T>, M extends FunctionPropertyNames<Y>>(object: T, method: M): Y[M] extends (...args: any[]) => any ? SpyInstance<ReturnType<Y[M]>, ArgsType<Y[M]>> : never;

@antoinebrault : :+1: Thank you for fixing this issue. If possible, could you please update the example in the definition file with proper typing.

Was this page helpful?
0 / 5 - 0 ratings