100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Testing

Writing Custom Matchers

Learn how to extend Jest's expect with expect.extend() to write custom matchers that produce clearer, more expressive assertions and failure messages.

Advanced JestAdvanced9 min readJul 10, 2026
Analogies

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.

javascript
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

Was this page helpful?

Topics covered

#Testing#JestStudyNotes#TestingQA#WritingCustomMatchers#Writing#Custom#Matchers#Write#StudyNotes#SkillVeris