0

I'm using fp-ts and I have a function that returns either an HttpError object or a string:

async getPreferencesForUserId(userId: string): Promise<Either<HttpResponseNotFound, string>> {
    const preferences = await getRepository(Preference).findOne({ userId });
    return preferences ? right(preferences.preferenceMap) : left(new HttpResponseNotFound({ code: 404, message: 'Could not find preferences' }));
  }

I would like to call this function in another file like this:

const preferenceMapAsJsonStringOrError: Either<HttpResponseNotFound, string> = await this.preferenceService.getPreferencesForUserId(userId);

const response: HttpResponseOK | HttpResponseNotFound = pipe(preferenceMapAsJsonStringOrError, fold(
  e => e,
  r => new HttpResponseOK(r)
));
response.setHeader('content-type', 'application/json');

return response;

This is basically how I'd do it in Scala. (with the exception that fold is a method on the Either type, not a standalone function - so here I'm using the pipe helper)

The problem is, I'm getting an error from ts-server:

Type 'HttpResponseOK' is missing the following properties from type 'HttpResponseNotFound': isHttpResponseNotFound, isHttpResponseClientError

And

node_modules/fp-ts/lib/Either.d.ts:129:69                                                                           
    129 export declare function fold<E, A, B>(onLeft: (e: E) => B, onRight: (a: A) => B): (ma: Either<E, A>) => B;
                                                                            ~~~~~~~~~~~
    The expected type comes from the return type of this signature.

I can get around this issue by doing this in a much more imperative way:

const preferenceMapAsJsonStringOrError: Either<HttpResponseNotFound, string> = await this.preferenceService.getPreferencesForUserId(userId);
if (isLeft(preferenceMapAsJsonStringOrError)) {
  return preferenceMapAsJsonStringOrError.left;
}

const response = new HttpResponseOK(preferenceMapAsJsonStringOrError.right);
response.setHeader('content-type', 'application/json');

return response;

But I pretty much lose the benefit of using an Either at that point.

Jim Wharton
  • 1,375
  • 3
  • 18
  • 41

2 Answers2

4

The issue is that, given how TS inference works, when using fold, its return type is "fixed" to the one of the first argument (onLeft), and onRight is not able to "widen" it HttpResponseNotFound | HttpResponseOK.

In other words, you will not get unification for free in the general case using TS and fp-ts.

For this specific scenario, I would suggest to

  1. give a name to the union type you want in output (not strictly necessary, but helps in clarifying intent):
type HttpResponse = HttpResponseNotFound | HttpResponseOK
  1. explicitly "widen" the return type of fold. This has to be done manually, either by annotating the return type of the onLeft fold argument:
const response: HttpResponse = pipe(
  preferenceMapAsJsonStringOrError,
  E.fold((e): HttpResponse => e, r => new HttpResponseOK(r))
)

or by defining a widen helper as follows:

const widen = E.mapLeft<HttpResponse, HttpResponse>(e => e);

const response: HttpResponse = pipe(
  preferenceMapAsJsonStringOrError,
  widen,
  E.fold(identity, r => new HttpResponseOK(r))
);

Hope this helps :)

Giovanni Gonzaga
  • 1,185
  • 9
  • 8
1

I still get type errors after when trying the 2 methods. What fixed it for me was explicitly specifying the type of the fold.

fold<HttpResponseNotFound, HttpResponseOK, HttpResponseNotFound | HttpResponseOK>(
  e => e,
  r => new HttpResponseOK(r)
)
iflp
  • 1,742
  • 17
  • 25