Are fp-ts and Jest the perfect pairing for testing Option and Either types with ease?

When working with fp-ts, and conducting unit tests using Jest, I often come across scenarios where I need to test nullable results, typically represented by Option or Either (usually in array find operations). What is the most efficient way to ensure that the test fails if the result is 'none' (taking Option as an example), and continue without interruption if the result is 'some'?

Let's consider a solution I currently utilize:

function someFunc(input: string): Option.Option<string> {
  return Option.some(input);
}

describe(`Some suite`, () => {
  it(`should perform actions with a "some" result`, () => {
    const result = someFunc('abcd');

    // This is a failure scenario, expecting result to be Some
    if(Option.isNone(result)) {
      expect(Option.isSome(result)).toEqual(true);
      return;
    }

    expect(result.value).toEqual('abcd');
  });
});

However, including an if statement with an early return can be cumbersome.

Another approach could involve using an as assertion:

  // ...
  it(`should perform actions with a "some" result`, () => {
    const result = someFunc('abcd') as Option.Some<string>;

    expect(result.value).toEqual('abcd');
  });
  // ...

Yet, this method requires rewriting the type associated with some. In many instances, this may be arduous, necessitating the creation and export of interfaces solely for testing purposes (which is not very user-friendly).

Is there a simpler way to streamline this type of testing?

Edit: Here's a test case that closely resembles real-world conditions:


interface SomeComplexType {
  id: string,
  inputAsArray: string[],
  input: string;
}

function someFunc(input: string): Option.Option<SomeComplexType> {
  return Option.some({
    id: '5',
    inputAsArray: input.split(''),
    input,
  });
}

describe(`Some suite`, () => {
  it(`should perform actions with a "some" result`, () => {
    const result = someFunc('abcd');

    // This step lacks efficiency
    if(Option.isNone(result)) {
      expect(Option.isSome(result)).toEqual(true);
      return;
    }

    // Ideally, only this would be necessary:
    expect(Option.isSome(result)).toEqual(true);
    // Since no further steps will be executed if the result is not "some"
    // Though, it might be challenging for TS to deduce this from Jest expects

    // With knowledge of the actual value's type, numerous actions can be taken
    // Eliminating the need to check for nullability or specify its type
    const myValue = result.value;

    expect(myValue.inputAsArray).toEqual(expect.arrayContaining(['a', 'b', 'c', 'd']));

    const someOtherThing = getTheOtherThing(myValue.id);

    expect(someOtherThing).toMatchObject({
      another: 'thing',
    });
  });
});

Answer №1

Here is an example of how you can create an unsafe conversion function called fromSome:

function fromSome<T>(input: Option.Option<T>): T {
  if (Option.isNone(input)) {
    throw new Error();
  }
  return input.value;
}

You can then use this function in your tests like so:

  it(`should perform a task with a "some" result`, () => {
    const result = someFunc('abcd');
    const myValue = fromSome(result);
    // Do something with myValue
  });

Answer №2

Have you considered using toNullable or toUndefined? When given an Option<string>, toNullable will give back a result of type string | null.

import { toNullable, toUndefined } from "fp-ts/lib/Option";

it(`should perform a task with a "some" outcome`, () => {
  expect(toNullable(someFunc("abcd"))).toEqual("abcd");
});

An issue arises with

expect(Option.isSome(result)).toEqual(true)
since the type guard isSome cannot be utilized to narrow down result in the surrounding code area of expect (refer to this link for information on control flow analysis).

You can leverage assertion functions along with fp-ts type guards, for instance:

import { isSome, Option } from "fp-ts/lib/Option"

function assert<T>(guard: (o: any) => o is T, o: any): asserts o is T {
  if (!guard(o)) throw new Error() // alternatively include parameter for custom error
}

it(`a test`, () => {
  const result: Option<string> = {...}
  assert(isSome, result)
  // at this point, result is narrowed down to type "Some"
  expect(result.value).toEqual('abcd');
});

I am uncertain if there exists an efficient method of enhancing the Jest expect function type itself with a type guard signature, but I suspect it may not simplify your scenario compared to a straightforward assertion or the solutions mentioned above.

Answer №3

Although this question may be a bit dated and already has an accepted answer, there are a couple of fantastic libraries that streamline testing with `fp-ts` `Either` and `Option`. Both of these libraries perform exceptionally well, leaving me torn between which one I prefer.

With these libraries, you can easily write tests like the following:

test('some test', () => {
  expect(E.left({ code: 'invalid' })).toSubsetEqualLeft({ code: 'invalid' })
})

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Collaborate and apply coding principles across both Android and web platforms

Currently, I am developing a web version for my Android app. Within the app, there are numerous utility files such as a class that formats strings in a specific manner. I am wondering if there is a way to write this functionality once and use it on both ...

The argument provided needs to be a function, but instead, an object instance was received, not the original argument as expected

I originally had the following code: const util = require('util'); const exec = util.promisify(require('child_process').exec); But then I tried to refactor it like this: import * as exec from 'child_process'; const execPromis ...

Retrieve the text content of the <ul> <li> elements following a click on them

Currently, I am able to pass the .innerTXT of any item I click in my list of items. However, when I click on a nested item like statistics -> tests, I want to display the entire path and not just 'tests'. Can someone assist me in resolving thi ...

What happens when the loading state does not update while using an async function in an onClick event?

I'm currently working on implementing the MUI Loading Button and encountering an issue with changing the loading state of the button upon click. Despite setting the state of downloadLoading to true in the onClick event, it always returns false. The p ...

Utilize Array in Form Input with Index and Spread Operator

Looking to create a form that can handle array data with dynamic fields in TypeScript. Encountering the following error: Element implicitly has an 'any' type because expression of type 'any' can't be used to index type '{ nam ...

TSX implementation of a paginator with an ellipse in the center

Looking to add ellipses in the Pagination, specifically when there are more than 10 pages (e.g., 1 2 3 4 ... 11 12 13 14). I've tried various methods but need guidance as a beginner. Can anyone suggest changes based on my code to help me achieve this? ...

The declaration file for the 'express' module could not be located

Whenever I run my code to search for a request and response from an express server, I encounter an issue where it cannot find declarations for the 'express' module. The error message transitions from Could not find a declaration file for module & ...

Combining Vue-Test-Utils with TypeScript typings for wrapper.vm

So, I ran into an interesting situation. Has anyone ever worked with typescript + vue-test-utils and attempted to change a value for testing purposes like this: wrapper.vm.aCoolRefValueToManipulate = 'something much cooler'? I gave it a shot, a ...

Setting up "connect-redis" in a TypeScript environment is a straightforward process

Currently, I am diving into the Fullstack React GraphQL TypeScript Tutorial I encountered an issue while trying to connect Redis with express-session... import connectRedis from "connect-redis"; import session from "express-session"; ...

Ways to retrieve the initial 4 elements from an array or class organized by their price entries in ascending order

Let's say we have an array of objects representing products: Products: Product[] = [ { id: 1, name: 'Milk', price: '1' }, { id: 2, name: 'Flour', price: '20' }, { id: 3, name: 'Jeans', pri ...

Exploring the functionality of the Angular 7 date pipe in a more dynamic approach by incorporating it within a template literal using backticks, specifically

I have a value called changes.lastUpdatedTime.currentValue which is set to 1540460704884. I am looking to format this value using a pipe for date formatting. For example, I want to achieve something like this: {{lastUpdatedTime | date:'short'}} ...

Tips for simulating redux-promise-listener Middleware and final-form

I recently configured react-redux-promise-listener using the (repository) to work with react-final-form following the author's instructions. However, I am facing difficulties when trying to mock it for testing purposes. The error message I'm enc ...

I encountered a mistake: error TS2554 - I was expecting 1 argument, but none was given. Additionally, I received another error stating that an argument for 'params' was not provided

customer-list.component.ts To load customers, the onLoadCustomers() function in this component calls the getCustomers() method from the customer service. customer.servise.ts The getCustomers() method in the customer service makes a POST request to the A ...

Angular Group Formation Issue

I keep encountering the following error: formGroup expects a FormGroup instance. Please pass one in. This is how it looks in HTML: <mat-step [stepControl]="firstFormGroup"> <form [formGroup]="firstFormGroup"> And in my Typ ...

Angular is showing an error indicating that the property "name" is not found on an empty object

After thorough checking, I have confirmed that the property does exist with the correct key. However, it is returning an error message stating name is not a property of {}. I attempted to assign this object to an interface along with its properties but enc ...

React - Component not updating after Axios call in separate file

Recently I decided to delve into React while working on some R&D projects. One of my goals was to build an application from scratch as a way to learn and practice with the framework. As I started working on my project, I encountered a rather perplexin ...

Obtain information from a JSON file based on a specific field in Angular

The structure of the JSON file is as follows: localjson.json { "Product" :{ "data" : [ { "itemID" : "1" , "name" : "Apple" , "qty" : "3" }, { "itemID" : "2" , "name" : "Banana" , "qty" : "10" } ] ...

Tips on troubleshooting the issue when attempting to use a hook in your code

I am trying to implement a hook to manage the states and event functions of my menus. However, when I try to import the click function in this component, I encounter the following error: "No overload matches this call. The first of two overloads, '(p ...

Is it normal for TypeScript to not throw an error when different data types are used for function parameters?

function add(a:number, b:number):number { return a+b; } let mynumber:any = "50"; let result:number = add(mynumber, 5); console.log(result); Why does the console print "505" without throwing an error in the "add" function? If I had declared mynumber ...

Opening and closing a default Bootstrap modal in Angular 2

Instead of using angular2-bootstrap or ng2-bs3-modal as recommended in the discussions on Angular 2 Bootstrap Modal and Angular 2.0 and Modal Dialog, I want to explore other options. I understand how the Bootstrap modal opens and closes: The display pro ...