44

I regularly have unit tests where I need to compare two moment objects. I'd us moment's built-in function moment.isSame(moment) to compare them. However, this means my assertion will look like this:

expect(moment1.isSame(moment2)).toBeTrue();

I didn't quite like this, especially because the failure message will be less informative. Hence, I wanted to write a custom jest matcher "toBeSameMoment". The following code seems to compile at least:

import moment from "moment";

declare global {
  namespace jest {
    interface MomentMatchers extends Matchers<moment.Moment> {
      toBeSameMoment: (expected: moment.Moment) => CustomMatcherResult;
    }
  }
}

expect.extend({
  toBeSameMoment(received: moment.Moment, expected: moment.Moment): jest.CustomMatcherResult {
    const pass: boolean = received.isSame(expected);
    const message: () => string = () => pass ? "" : `Received moment (${received.toISOString()}) is not the same as expected (${expected.toISOString()})`;

    return {
      message,
      pass,
    };
  },
});

However, I can't really get it to work in my unit test... When I try the following test code:

import moment from "moment";
import "../jest-matchers/moment";

describe("Moment matcher", () => {

  test("should fail", () => {
    const moment1 = moment.utc();
    const moment2 = moment();

    expect(moment1).toBeSameMoment(moment2);
  });

});

...then I get the following error:

error TS2339: Property 'toBeSameMoment' does not exist on type 'JestMatchersShape<Matchers<void, Moment>, Matchers<Promise<void>, Moment>>'.

I don't quite get this error, though. For example, what is the void type this is referring to? I've tried googling about it, but didn't really find a good guide or so. I did take notice of this question: How to let know typescript compiler about jest custom matchers?, which seems to basically be a duplicate, but apparently not clear enough, yet.

I have jest types included in my tsconfig

skyboyer
  • 22,209
  • 7
  • 57
  • 64
Martao
  • 795
  • 2
  • 6
  • 12

1 Answers1

35

The other question and answer you linked to were correct, and you can also find a very succinct example for how to extend jest in this github comment on react-testing-library.

To implement their solution for your code, just change:

declare global {
  namespace jest {
    interface MomentMatchers extends Matchers<moment.Moment> {
      toBeSameMoment: (expected: moment.Moment) => CustomMatcherResult;
    }
  }
}

To:

declare global {
  namespace jest {
    interface Matchers<R> {
      toBeSameMoment(expected: moment.Moment): CustomMatcherResult;
    }
  }
}
Sean Anderson
  • 27,963
  • 30
  • 126
  • 237
Joshua T
  • 2,439
  • 1
  • 10
  • 42
  • Indeed, this works. Thanks! Can you explain me what this 'R' refers to? – Martao Feb 18 '20 at 16:36
  • 2
    `<_>` denotes a generic in TypeScript, which is basically a placeholder for a type to be filled in by other code. In this specific scenario, I'm guessing they used `R` to stand for **R**eceived, which would make sense, because if you look at `@types/jest/index.d.ts`, you can see they also use `` in methods defined on Matchers, where `E` is for the type of the expected value. – Joshua T Feb 19 '20 at 16:39
  • Okay, that starts to make some sense to me... I kind of expected I needed to provide the type parameter myself, but if R means received, that kind of explains. The type of the expected parameter needs to be of the same type and therefor the type can be inferred from the object received by the expect method. – Martao Feb 20 '20 at 12:54
  • 9
    In Typescript v4 [@typescript-eslint/no-namespace](https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/docs/rules/no-namespace.md) fails with `ES2015 module syntax is preferred over custom TypeScript modules and namespaces`. Is there a better approach than simply disabling this rule? Or is disabling necessary as long as `jest` is global? – Raine Revere Oct 28 '20 at 23:30
  • 7
    Where do you put jest.d.ts to make this solution actually work? – Steve Oct 20 '21 at 09:10
  • What if I *do* want to parameterize my custom matcher generically? E.g., what if I want to write a generic matcher that checks certain fields are the same, one that I could re-use across different objects? So I can use it like `toHaveSameFields<{ foo: string, bar: number }>(expected, 'foo', 'bar')`? The type of `expected` would need to be generic, not hardcoded like `moment.Moment`, but I can't figure out a way to do that that doesn't throw type errors. – Sasgorilla Dec 10 '21 at 20:01
  • @Sasgorilla I get an error like that when the file extension is `tsx` but not when `ts`. @Steve the same module resolution rules apply as with any `.d.ts` file. You could just directly import the file, too. – Kevin Beal Sep 14 '22 at 18:30