gcanti / fp-ts

Functional programming in TypeScript
https://gcanti.github.io/fp-ts/
MIT License
10.85k stars 503 forks source link

`RTE.fromReader` seems to erase errors from subsequent `RTE.chainW` #1706

Open atomanyih opened 2 years ago

atomanyih commented 2 years ago

🐛 Bug report

Current Behavior

Hullo again with another issue. Please excuse me if I'm totally misunderstanding what's going on here!

type Eater = {
  type: 'eater';
  name: string;
};

type VeganError = {
  type: 'VeganError';
};

type Sausage = {
  type: 'sausage';
  length: 'small' | 'medium' | 'large';
  name: string;
};

type SausageEmporium = {
  getSausage: (length: Sausage['length']) => Sausage;
};

const getSausage = (
  length: Sausage['length'],
): R.Reader<{ sausageStore: SausageEmporium }, Sausage> => {
  return pipe(
    R.ask<{ sausageStore: SausageEmporium }>(),
    R.map(({ sausageStore }) => sausageStore.getSausage(length)),
  );
};

const eatSausage = (
  sausage: Sausage,
): RTE.ReaderTaskEither<{ eater: Eater }, VeganError, void> => {
  return RTE.of(constVoid());
};

const getAndEatSausage = (
  length: Sausage['length'],
): RTE.ReaderTaskEither<
  { eater: Eater; sausageStore: SausageEmporium },
  VeganError,
  void
> => {
  // ERROR:
  // TS2322: Type 'ReaderTaskEither<{ sausageStore: SausageEmporium; } & { eater: Eater; }, unknown, void>' is not assignable to type 'ReaderTaskEither<{ eater: Eater; sausageStore: SausageEmporium; }, VeganError, void>'. 
  //  Type 'unknown' is not assignable to type 'VeganError'.
  return pipe(
    getSausage(length),
    RTE.fromReader,
    RTE.chainW(eatSausage)
  );
};

It seems that fromReader returns ReaderTaskEither with unknown as the left (error) parameter. If this is then chained, the error type is unknown | VeganError which reduces to unknown, erasing chainWed error types.

Expected behavior

The above code should compile

Suggested solution(s)

I might expect fromReader to return never as the error parameter, as it can never error. This would also properly union when widened with chainW.

See working example where I substitute the fromReader with a never-error RTE:

const neverExample = (
  length: Sausage['length'],
): RTE.ReaderTaskEither<
  { eater: Eater; sausageStore: SausageEmporium },
  VeganError,
  void
> => {
  const readerTaskEither: RTE.ReaderTaskEither<
    { sausageStore: SausageEmporium },
    never,
    Sausage
  > = RTE.of({
    type: 'sausage',
    length: 'small',
    name: 'randy',
  });
  return pipe(
    readerTaskEither,
    RTE.chainW(eatSausage)
  );
};

Your environment

Software Version(s)
fp-ts 2.11.8
TypeScript 4.5.5
waynevanson commented 2 years ago

Wow, what a code example. Would you be able to share the background of the code? I'm intrigued. Optional, not required for help.

FromReader cannot infer from the parent signature if there is more than one function in the pipe.

const getAndEatSausge = (length: Sausage["length"]) => pipe(
  RTE.fromReader(getSausage(length)) as RTE.ReaderChainEither<Sausage, VeganError, Sausage>,
  RTE.chainW(eatSausage)
)
ankrut commented 2 years ago

Alternatively there is RTE.rightReader where the default error type is never such that TS can do the job.

pipe(
   getSausage(length),
   RTE.rightReader,
   RTE.chainW(eatSausage)
)