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!