I am trying to add a custom matcher to Jest in Typescript. This works fine, but I can't get Typescript to recognize the extended Matchers
.
myMatcher.ts
export default function myMatcher (this: jest.MatcherUtils, received: any, expected: any): { pass: boolean; message (): string; } { const pass = received === expected; return { pass: pass, message: () => `expected ${pass ? '!' : '='}==`, } }
myMatcher.d.ts
declare namespace jest { interface Matchers { myMatcher (expected: any): boolean; } }
someTest.ts
import myMatcher from './myMatcher'; expect.extend({ myMatcher, }) it('should work', () => { expect('str').myMatcher('str'); })
tsconfig.json
{ "compilerOptions": { "outDir": "./dist/", "moduleResolution": "node", "module": "es6", "target": "es5", "lib": [ "es7", "dom" ] }, "types": [ "jest" ], "include": [ "src/**/*" ], "exclude": [ "node_modules", "dist", "doc", "**/__mocks__/*", "**/__tests__/*" ] }
In someTests.ts, I get the error
error TS2339: Property 'myMatcher' does not exist on type 'Matchers'
I have read through the Microsoft documentation several times, but I can't figure out how to do the namespace merging with globally available types (not exported).
Putting it in the index.d.ts from jest works fine, but isn't a good solution for a rapidly changing codebase and classes being extended by multiple parties.
OK, so there are a few issues here
When a source file (.ts
or .tsx
) file and a declaration file (.d.ts
) file are both candidates for module resolution, as is the case here, the compiler will resolve the source file.
You probably have two files because you want to export a value and also modify the type of the global object jest
. However, you do not need two files for this as TypeScript has a specific construct for augmenting the global scope from within a module. That is to say, all you need is the following .ts
file
myMatcher.ts
// use declare global within a module to introduce or augment a global declaration. declare global { namespace jest { interface Matchers { myMatcher: typeof myMatcher; } } } export default function myMatcher<T>(this: jest.MatcherUtils, received: T, expected: T) { const pass = received === expected; return { pass, message: () => `expected ${pass ? '!' : '='}==` }; }
That said, if you have such a situation, it is a good practice to perform the global mutation and the global type augmentation in the same file. Given that, I would consider rewriting it as follows
myMatcher.ts
// ensure this is parsed as a module. export {}; declare global { namespace jest { interface Matchers { myMatcher: typeof myMatcher; } } } function myMatcher<T>(this: jest.MatcherUtils, received: T, expected: T) { const pass = received === expected; return { pass, message: () => `expected ${pass ? '!' : '='}==` }; } expect.extend({ myMatcher });
someTest.ts
import './myMatcher'; it('should work', () => { expect('str').myMatcher('str'); });
A simple way is:
customMatchers.ts
declare global { namespace jest { interface Matchers<R> { // add any of your custom matchers here toBeDivisibleBy: (argument: number) => {}; } } } // this will extend the expect with a custom matcher expect.extend({ toBeDivisibleBy(received: number, argument: number) { const pass = received % argument === 0; if (pass) { return { message: () => `expected ${received} not to be divisible by ${argument}`, pass: true }; } else { return { message: () => `expected ${received} to be divisible by ${argument}`, pass: false }; } } });
my.spec.ts
import "path/to/customMatchers"; test('even and odd numbers', () => { expect(100).toBeDivisibleBy(2); expect(101).not.toBeDivisibleBy(2); });
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With