From c9625d458c59753356a976441e454ee2e99d7538 Mon Sep 17 00:00:00 2001 From: Vladimir Sheremet Date: Wed, 14 Jan 2026 15:59:53 +0100 Subject: [PATCH] feat: expose matcher types --- docs/guide/extending-matchers.md | 89 +++++++++++++++++++---------- packages/vitest/src/public/index.ts | 3 + 2 files changed, 63 insertions(+), 29 deletions(-) diff --git a/docs/guide/extending-matchers.md b/docs/guide/extending-matchers.md index 8eb5fceec825..f811c980d9f2 100644 --- a/docs/guide/extending-matchers.md +++ b/docs/guide/extending-matchers.md @@ -4,7 +4,7 @@ title: Extending Matchers | Guide # Extending Matchers -Since Vitest is compatible with both Chai and Jest, you can use either the `chai.use` API or `expect.extend`, whichever you prefer. +Since Vitest is compatible with both Chai and Jest, you can use either the [`chai.use`](https://www.chaijs.com/guide/plugins/) API or `expect.extend`, whichever you prefer. This guide will explore extending matchers with `expect.extend`. If you are interested in Chai's API, check [their guide](https://www.chaijs.com/guide/plugins/). @@ -23,38 +23,24 @@ expect.extend({ }) ``` -If you are using TypeScript, you can extend default `Assertion` interface in an ambient declaration file (e.g: `vitest.d.ts`) with the code below: +If you are using TypeScript, you can extend default `Matchers` interface in an ambient declaration file (e.g: `vitest.d.ts`) with the code below: -::: code-group -```ts [3.2.0] +```ts import 'vitest' -interface CustomMatchers { - toBeFoo: () => R -} - declare module 'vitest' { - interface Matchers extends CustomMatchers {} -} -``` -```ts [3.0.0] -import 'vitest' - -interface CustomMatchers { - toBeFoo: () => R -} - -declare module 'vitest' { - interface Assertion extends CustomMatchers {} - interface AsymmetricMatchersContaining extends CustomMatchers {} + interface Matchers { + toBeFoo: () => R + } } ``` -::: ::: tip -Since Vitest 3.2, you can extend the `Matchers` interface to have type-safe assertions in `expect.extend`, `expect().*`, and `expect.*` methods at the same time. Previously, you had to define separate interfaces for each of them. +Importing `vitest` makes TypeScript think this is an ES module file, type declaration won't work without it. ::: +Extending the `Matchers` interface will add a type to `expect.extend`, `expect().*`, and `expect.*` methods at the same time. + ::: warning Don't forget to include the ambient declaration file in your `tsconfig.json`. ::: @@ -62,7 +48,7 @@ Don't forget to include the ambient declaration file in your `tsconfig.json`. The return value of a matcher should be compatible with the following interface: ```ts -interface ExpectationResult { +interface MatcherResult { pass: boolean message: () => string // If you pass these, they will automatically appear inside a diff when @@ -73,7 +59,7 @@ interface ExpectationResult { ``` ::: warning -If you create an asynchronous matcher, don't forget to `await` the result (`await expect('foo').toBeFoo()`) in the test itself:: +If you create an asynchronous matcher, don't forget to `await` the result (`await expect('foo').toBeFoo()`) in the test itself: ```ts expect.extend({ @@ -86,13 +72,46 @@ await expect().toBeAsyncAssertion() ``` ::: -The first argument inside a matcher's function is the received value (the one inside `expect(received)`). The rest are arguments passed directly to the matcher. +The first argument inside a matcher's function is the received value (the one inside `expect(received)`). The rest are arguments passed directly to the matcher. Since version 4.1, Vitest exposes several types that can be used by your custom matcher: + +```ts +import type { + // the function type + Matcher, + // the return value + MatcherResult, + // state available as `this` + MatcherState, +} from 'vitest' +import { expect } from 'vitest' + +// a simple matcher, using "function" to have access to "this" +const customMatcher: Matcher = function (received) { + // ... +} + +// a matcher with arguments +const customMatcher: Matcher = function (received, arg1, arg2) { + // ... +} + +// a matcher with custom annotations +function customMatcher(this: MatcherState, received: unknown, arg1: unknown, arg2: unknown): MatcherResult { + // ... + return { + pass: false, + message: () => 'something went wrong!', + } +} + +expect.extend({ customMatcher }) +``` Matcher function has access to `this` context with the following properties: ### `isNot` -Returns true, if matcher was called on `not` (`expect(received).not.toBeFoo()`). +Returns true, if matcher was called on `not` (`expect(received).not.toBeFoo()`). You do not need to respect it, Vitest will reverse the value of `pass` automatically. ### `promise` @@ -112,7 +131,7 @@ This contains a set of utility functions that you can use to display messages. Full name of the current test (including describe block). -### `task` 4.0.11 {#task} +### `task` 4.1.0 {#task} Contains a reference to [the `Test` runner task](/api/advanced/runner#tasks) when available. @@ -122,4 +141,16 @@ When using the global `expect` with concurrent tests, `this.task` is `undefined` ### `testPath` -Path to the current test. +File path to the current test. + +### `environment` + +The name of the current [`environment`](/config/environment) (for example, `jsdom`). + +### `soft` + +Was assertion called as a [`soft`](/api/expect#soft) one. You don't need to respect it, Vitest will always catch the error. + +::: tip +These are not all of the available properties, only the most useful ones. The other state values are used by Vitest internally. +::: diff --git a/packages/vitest/src/public/index.ts b/packages/vitest/src/public/index.ts index 6ee9bdbdab38..4becb8f98761 100644 --- a/packages/vitest/src/public/index.ts +++ b/packages/vitest/src/public/index.ts @@ -92,7 +92,10 @@ export type { ExpectPollOptions, ExpectStatic, JestAssertion, + RawMatcherFn as Matcher, + ExpectationResult as MatcherResult, Matchers, + MatcherState, } from '@vitest/expect' export { afterAll,