Skip to content

Instantly share code, notes, and snippets.

@kettanaito
Created July 15, 2026 21:53
Show Gist options
  • Select an option

  • Save kettanaito/24540d794c11057619ebe3da21826854 to your computer and use it in GitHub Desktop.

Select an option

Save kettanaito/24540d794c11057619ebe3da21826854 to your computer and use it in GitHub Desktop.
expect.optional
import { isDeepStrictEqual } from 'node:util'
import { expect } from 'vitest'
class OptionalMatcher {
readonly $$typeof = Symbol.for('jest.asymmetricMatcher')
constructor(private readonly expected: unknown) {}
asymmetricMatch(received: unknown): boolean {
if (received === undefined) {
return true
}
return isDeepStrictEqual(received, this.expected)
}
toString(): string {
return 'Optional'
}
getExpectedType(): string {
return 'any'
}
toAsymmetricMatcher(): string {
return `Optional<${JSON.stringify(this.expected)}>`
}
}
function isPlainObject(value: unknown): value is Record<string, unknown> {
if (value === null || typeof value !== 'object') {
return false
}
if (Array.isArray(value)) {
return false
}
if (
'$$typeof' in value &&
value.$$typeof === Symbol.for('jest.asymmetricMatcher')
) {
return false
}
const prototype = Object.getPrototypeOf(value)
return prototype === null || prototype === Object.prototype
}
/**
* Custom equality testers:
*
* 1. Trailing-optional arrays — when one array is longer than the other and
* every "extra" slot in the longer one is an `OptionalMatcher`, treat
* those slots as absent and compare only the shared prefix.
*
* 2. Missing-optional object keys — when a key is present on one object but
* absent on the other AND its value on the present side is an
* `OptionalMatcher`, consider the key absent on both sides.
*
* Both are needed because vitest's default deep-equality rejects on size
* mismatch (array length, key count) BEFORE consulting asymmetric matchers,
* so the matcher's "received === undefined ⇒ true" branch never gets a
* chance to run otherwise.
*/
expect.addEqualityTesters([
function trailingOptional(a, b, customTesters) {
if (!Array.isArray(a) || !Array.isArray(b)) {
return undefined
}
if (a.length === b.length) {
return undefined
}
const [shorter, longer] = a.length < b.length ? [a, b] : [b, a]
for (let index = shorter.length; index < longer.length; index++) {
if (!(longer[index] instanceof OptionalMatcher)) {
return undefined
}
}
for (let index = 0; index < shorter.length; index++) {
if (!this.equals(a[index], b[index], customTesters)) {
return false
}
}
return true
},
function missingOptionalKey(a, b, customTesters) {
if (!isPlainObject(a) || !isPlainObject(b)) {
return undefined
}
let hasMissingOptional = false
for (const key of Object.keys(a)) {
if (!(key in b) && a[key] instanceof OptionalMatcher) {
hasMissingOptional = true
break
}
}
if (!hasMissingOptional) {
for (const key of Object.keys(b)) {
if (!(key in a) && b[key] instanceof OptionalMatcher) {
hasMissingOptional = true
break
}
}
}
if (!hasMissingOptional) {
return undefined
}
const allKeys = new Set<string>([...Object.keys(a), ...Object.keys(b)])
for (const key of allKeys) {
if (!this.equals(a[key], b[key], customTesters)) {
return false
}
}
return true
},
])
Object.defineProperty(expect, 'optional', {
value: <T>(expected: T): OptionalMatcher => new OptionalMatcher(expected),
})
declare module 'vitest' {
interface AsymmetricMatchersContaining {
/**
* Always accepts a value.
* Passes if no value is present at its place.
* If a value is present, matches it for equality and passes only if equal.
*/
optional<T>(expected: T): unknown
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment