When a domain-specific check comes up repeatedly, a custom matcher can make your tests more readable than repeating the same manual logic everywhere. This lesson covers expect.extend() and how to write a well-behaved custom matcher.
Writing a Matcher with expect.extend()
expect.extend({ matcherName(received, expected) {...} }) registers a new matcher usable anywhere expect() is used. Your matcher function must return an object with pass (a boolean) and message (a function returning the failure message shown when the matcher doesn't behave as expected).
Register custom matchers once, typically in a setupFilesAfterEnv file, so they're available across your whole test suite without repeated imports.
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${received} ${pass ? 'not ' : ''}to be within range ${floor} - ${ceiling}`,
};
},
});
test('discount is within an acceptable range', () => {
expect(calculateDiscount(order)).toBeWithinRange(0, 50);
});
The custom matcher reads just like a built-in one and produces a clear, purpose-built failure message.
received is always the value passed to expect(); remaining arguments are whatever you pass to the matcher.
pass must be a boolean; Jest automatically handles .not by inverting the interpretation.
message() should describe the failure clearly, ideally mentioning both received and expected values.
TypeScript projects should also extend the jest.Matchers interface for type-safe custom matchers.
Custom Matchers Cheat Sheet
The essentials of building your own Jest matcher.
Piece
Purpose
expect.extend({...})
Registers one or more custom matchers
received
The value expect() was called with
pass
Boolean result of your check
message()
Function returning the failure message
this.utils.printReceived(x)
Jest's built-in colorized value formatting
Register in setupFilesAfterEnv
Makes the matcher available everywhere
Using this.utils for Consistent Formatting
Inside a matcher function, this.utils provides helpers like printReceived() and printExpected() that format values the same way Jest's built-in matchers do (with the same coloring and styling), keeping your custom matcher's failure output visually consistent with the rest of the suite.
expect.extend({
toBeWithinRange(received, floor, ceiling) {
const pass = received >= floor && received <= ceiling;
return {
pass,
message: () =>
`expected ${this.utils.printReceived(received)} to be within range ${floor} - ${ceiling}`,
};
},
});
Building Domain-Specific Matchers
Custom matchers shine for domain concepts that come up repeatedly across a codebase — checking a valid email format, a properly shaped API error object, or a specific business rule — turning a multi-line manual check into a single, readable assertion.
Forgetting message() must be a function, not a plain string.
Not handling the .not case correctly (Jest handles inversion automatically as long as pass is accurate).
Writing an overly specific one-off matcher instead of just using an existing built-in matcher.
Skipping TypeScript type declarations for custom matchers, losing autocomplete and type safety.
Key Takeaways
expect.extend() registers custom matchers usable exactly like built-in ones.
Each matcher returns { pass, message }, with message as a function.
this.utils provides consistent value formatting matching Jest's built-in style.
Custom matchers are best reserved for domain concepts that recur across many tests.
Pro Tip
Before writing a custom matcher, check if jest-extended (a popular community matcher library) already provides what you need — many common domain checks are already covered.
You now know how to write custom Jest matchers. Next, learn about test doubles and the different types beyond mocks and spies.