Why Write a Custom Matcher
A custom matcher packages a reusable, domain-specific assertion, like toBeWithinRange(0, 100) or toHaveValidationError('email'), behind a single readable expect(...) call instead of scattering multiple raw comparisons and manual conditionals across many tests. Beyond readability, a well-written custom matcher also produces a tailored failure message that shows exactly what was expected versus received in domain terms, rather than the generic diff Jest would produce from a manual if statement combined with a plain toBe(true) assertion.
Cricket analogy: Similar to a commentator saying 'that's out LBW' instead of listing raw ball-tracking coordinates, a custom matcher like toBeWithinRange() expresses a domain-specific check in one readable call instead of several raw comparisons.
Implementing a Matcher with expect.extend()
expect.extend() registers one or more matcher functions on the global expect object, and each matcher function receives the received value as its first argument followed by any arguments passed at the call site, and must return an object with a boolean pass property and a message function that returns the string used for the failure output. Inside the matcher, this.isNot and this.utils, including this.utils.printReceived and this.utils.printExpected, are available to build a consistent, colorized failure message matching Jest's own built-in matcher style.
Cricket analogy: Similar to a third umpire's protocol requiring a clear verdict, out or not out, plus the specific evidence cited, an expect.extend() matcher must return a pass boolean plus a message function explaining the verdict.
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
const message = pass
? () =>
`expected ${this.utils.printReceived(received)} not to be within range ${floor} - ${ceiling}`
: () =>
`expected ${this.utils.printReceived(received)} to be within range ${floor} - ${ceiling}`;
return { pass, message };
},
});
test('score is within the valid range', () => {
expect(87).toBeWithinRange(0, 100);
expect(150).not.toBeWithinRange(0, 100);
});Handling .not and Asymmetric Matchers
Because Jest automatically negates the pass result when a matcher is called with .not, a well-written matcher's message function should check this.isNot to phrase the failure message correctly in both directions, rather than always describing the positive case even when the test used .not.toBeWithinRange(). Custom matchers can also be used as asymmetric matchers inside other matchers, such as expect.toBeWithinRange(0, 100) nested inside a toEqual() call against an object property, once they're registered with expect.extend(), because Jest automatically makes registered matchers available on both expect() and expect.<matcherName>().
Cricket analogy: Similar to a scorecard needing to phrase 'did not reach fifty' differently from 'reached fifty' depending on the actual outcome, a matcher's message function checks this.isNot to phrase the failure correctly whether .not was used or not.
Registered custom matchers automatically become available as asymmetric matchers via expect.<matcherName>(...args), so expect({ score: 87 }).toEqual({ score: expect.toBeWithinRange(0, 100) }) works once toBeWithinRange is registered, letting custom matchers nest inside other structural assertions.
Typing Custom Matchers in TypeScript
In a TypeScript project, calling a custom matcher like expect(87).toBeWithinRange(0, 100) fails to type-check unless you extend Jest's own matcher interfaces with a declaration merging block, typically placed in a .d.ts file, that adds the new matcher's signature to jest.Matchers<R> for the synchronous expect chain. Omitting this declaration doesn't break the test at runtime, since Jest itself doesn't care about TypeScript types, but it does produce compiler errors under strict type-checking, which is why teams commonly keep a jest-setup.d.ts file alongside their custom matcher implementation.
Cricket analogy: Similar to a scorer needing an official rulebook amendment before a new dismissal type like 'timed out' is recognized on the official scoresheet, TypeScript needs a declaration-merging block before it recognizes a custom matcher's signature.
Forgetting to include the jest-setup.d.ts declaration file in your tsconfig's include array, or forgetting to reference it from test files, means the ambient module augmentation never gets picked up, so TypeScript still reports 'Property toBeWithinRange does not exist on type Matchers' even though the file exists in the project.
- Custom matchers package a reusable, domain-specific check behind a single readable expect() call with a tailored failure message.
- expect.extend() registers matcher functions that must return an object with a boolean pass property and a message function.
- this.utils, including printReceived and printExpected, helps build failure messages consistent with Jest's built-in matcher style.
- A matcher's message function should check this.isNot to phrase output correctly for both positive and .not assertions.
- Registered matchers automatically become available as asymmetric matchers via expect.<matcherName>() nested inside other assertions.
- TypeScript requires a declaration-merging block extending jest.Matchers<R> for custom matchers to type-check.
- Omitting the TypeScript declaration doesn't break runtime behavior but produces compiler errors under strict type-checking.
Practice what you learned
1. What must a function registered with expect.extend() return?
2. What is the purpose of this.isNot inside a custom matcher's message function?
3. How does a registered custom matcher become usable as an asymmetric matcher?
4. Why might expect(87).toBeWithinRange(0, 100) fail to type-check in TypeScript even though it runs correctly?
5. What utility is commonly used inside a custom matcher to format the received value consistently with Jest's built-in matchers?
Was this page helpful?
You May Also Like
Testing Node.js APIs and Express Routes
Learn how to test Express routes and Node.js API handlers with Jest and supertest, including mocking databases, external calls, and error-handling middleware.
Jest Configuration: Multi-Project Setups
Learn how to configure Jest's projects array to run multiple test environments and configurations, like unit and integration suites, from a single Jest invocation.
Parameterized Tests with test.each()
Learn how to use Jest's test.each() and describe.each() to run the same test logic against multiple input/output pairs without duplicating code.