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!