Asserting Either in Vitest
"Enhancing TypeScript testing with custom matchers for FP-TS Either objects in Vitest."
Discovering the potential of the FP-TS library has been a rewarding experience, unlocking the power of functional programming. However, integrating it seamlessly can pose challenges. In my recent exploration of extending vitest matchers for the FP-TS ‘Either’ object, I encountered obstacles—particularly TypeScript’s reluctance to recognize custom matchers.
This post aims to share insights gained from overcoming these challenges, providing a guide for those grappling with TypeScript’s nuances or seeking to enhance their testing suite for FP-TS. Let’s dive in:
Ok. Let’s start with a super simple function that returns Either right when a number is positive or a Left with a message when the number is negative:
const isPositive = (n: number): E.Either<string, number> => {
	return n >= 0 ? E.right(n) : E.left('Negative number');
};How could we write a test for it?
import * as E from 'fp-ts/Either';
import { describe, it, expect } from 'vitest';
const isPositive = (n: number): E.Either<string, number> => {
	return n >= 0 ? E.right(n) : E.left('Negative number');
};
describe('Either', () => {
	it('isPositive', () => {
		const result = isPositive(1);
		expect(E.isRight(result)).toBe(true);
	});
});Using the isRight or isLeft utility from Either is great to
assert the result type but what about the actual value (right or left object value)?
Given the Either is a union type in Typescript the only way to gain access to the Right or Left object without errors is by checkin the tag first:
it('isPositive', () => {
	const result = isPositive(1);
	expect(E.isRight(result)).toBe(true);
	if (E.isRight(result)) {
		expect(result.right).toBe(1);
	}
});Having if statements in test code doesn’t feel very clean!
Wouldn’t it be nice if we could do both at once like this for left or right by calling a method like
toBeRightStrictEqual;which with is akin to the
toStrictEquals;Which ends up looking like this:
it('isPositive', () => {
	const result = isPositive(1);
	expect(result).toBeRightStrictEqual(1);
});
it('isNegative', () => {
	const result = isPositive(-1);
	expect(result).toBeLeftStrictEqual('Negative number');
});Easy. Vitest has it covered with custom matchers. First we’ll need the setup file for vitest config that has the actual matchers:
import * as E from 'fp-ts/Either';
import * as vitest from 'vitest';
declare module 'vitest' {
	interface CustomMatchers<R = unknown> {
		toBeRightStrictEqual(expected: any): R;
	}
}
vitest.expect.extend({
	toBeRightStrictEqual(received: E.Either<unknown, unknown>, expected: unknown) {
		return {
			pass: E.isRight(received) && this.equals(received.right, expected),
			message: () => `expected ${received} to be right ${expected}`
		};
	},
	toBeLeftStrictEqual(received, expected) {
		return {
			pass: E.isLeft(received) && this.equals(received.left, expected),
			message: () => `expected ${received} to be left ${expected}`
		};
	}
});Finally. We then need the the declarations file for Typescript. This file should be able to live anywhere in your src directory. I named it
vitest.extend.d.ts;Here’s the content:
interface CustomMatchers<R = unknown> {
	toBeRightStrictEqual(data: unknown): R;
	toBeLeftStrictEqual(data: unknown): R;
}
declare module 'vitest' {
	interface Assertion<T = any> extends CustomMatchers<T> {}
	interface AsymmetricMatchersContaining extends CustomMatchers {}
}
export {};Hope this helps others keep their code clean with some nicer Either assertions in Typescript!