question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. ItĀ collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Branching `if` statement in `Either.chainW` doesn't union properly

See original GitHub issue

šŸ› Bug report

Current Behavior

Note: example has been replaced with a more representative case

type StoreError = {
  type: 'storeError';
  excuse: string;
};

type Hotdog = {
  type: 'hotdog';
  length: 'small' | 'medium' | 'large';
  name: string;
};

const getHotdog = (length: Hotdog['length']): E.Either<StoreError, Hotdog> => {
  return E.of({
    type: 'hotdog',
    length,
    name: 'Cindy',
  })
}

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

const getSausage = (length: Sausage['length']): E.Either<StoreError, Sausage> => {
  return E.of({
    type: 'sausage',
    length,
    name: 'Addison',
  })
}

function getMediumHotdogOrSausage(type: 'hotdog' | 'sausage') {
  return pipe(
    E.of(type),
    E.chainW((type) => {
// TS2345: Argument of type '(type: "hotdog" | "sausage") => Left<StoreError> | Right<Hotdog> | Right<Sausage>' is not assignable to parameter of type '(a: "hotdog" | "sausage") => Either<StoreError, Hotdog>'.
//   Type 'Left<StoreError> | Right<Hotdog> | Right<Sausage>' is not assignable to type 'Either<StoreError, Hotdog>'.
//     Type 'Right<Sausage>' is not assignable to type 'Either<StoreError, Hotdog>'.
//       Type 'Right<Sausage>' is not assignable to type 'Right<Hotdog>'.
//         Type 'Sausage' is not assignable to type 'Hotdog'.
//           Types of property 'type' are incompatible.
//             Type '"sausage"' is not assignable to type '"hotdog"'.
      if (type === 'hotdog') {
        return getHotdog('medium');
      }

      return getSausage('small');
    }),
  );
}

Expected behavior

I would expect this to pass type-check and result in a type E.Either<StoreError, Sausage | Hotdog>.

Is there a better way to represent a branch like this?

Your environment

Which versions of fp-ts are affected by this issue? Did this work in previous versions of fp-ts?

Software Version(s)
fp-ts 2.11.8
TypeScript 4.5.5

Issue Analytics

  • State:open
  • Created a year ago
  • Comments:5

github_iconTop GitHub Comments

4reactions
mlegenhausencommented, May 2, 2022

Thanks for the better example.

This is not a fp-ts related problem. You can get the same error when exchanging Either for example with Array which maybe makes the problem more obvious.

import * as A from 'fp-ts/Array'

function getMediumHotdogOrSausage(type: 'hotdog' | 'sausage') {
  return pipe(
    A.of(type),
    A.chain(type => {
	  // Argument of type '(type: "hotdog" | "sausage") => string[] | number[]' is not assignable to parameter of type '(a: "hotdog" | "sausage") => string[]'.
      //  Type 'string[] | number[]' is not assignable to type 'string[]'.
      //    Type 'number[]' is not assignable to type 'string[]'.
      //      Type 'number' is not assignable to type 'string'.ts(2345)
      if (type === 'hotdog') {
        return A.of('abc')
      }

      return A.of(123)
    })
  )
}

The problem is simply that the first return type TypeScript sees is a string[] but you want to assign a number[] in the second one which typescript interprets in an error on your side. You can work around this error by providing a return type so typescript knows that you want to actually return a combined array type. In this case you can write (string | number)[].

So in your case you need to provide the return type E.Either<StoreError, Sausage | Hotdog> to your arrow function so typescript knows that it should combine Sausage and Hotdog and not treat every Either on its own.

1reaction
atomanyihcommented, May 11, 2022

Thanks for the explanation! So when you say ā€œit’s not an fp-ts-related problemā€ do you mean it’s inherent to the type system in some way? Because typescript doesn’t seem to have a problem doing if statement normally. This works fine:

function stupidFunctionForExample(a: number[]) {
  return pipe(
    a,
    (a) => {
      if (a.length > 1) {
        return a.map((v) => v.toString());
      }

      return a;
    }
  );
}

Underlying explanation aside, my solution to this problem was to write a custom combinator:

export function branchW<T, TRefined extends T, D1, D2, E1, E2, T1, T2>(
  predicate: (val: T) => val is TRefined,
  onFalse: (val: T) => RTE.ReaderTaskEither<D1, E1, T1>,
  onTrue: (val: TRefined) => RTE.ReaderTaskEither<D2, E2, T2>,
): <E0, D0>(
  rte: RTE.ReaderTaskEither<D0, E0, T>,
) => RTE.ReaderTaskEither<D0 & D1 & D2, E0 | E1 | E2, T1 | T2>;
export function branchW<T, D1, D2, E1, E2, T1, T2>(
  predicate: (val: T) => boolean,
  onFalse: (val: T) => RTE.ReaderTaskEither<D1, E1, T1>,
  onTrue: (val: T) => RTE.ReaderTaskEither<D2, E2, T2>,
): <E0, D0>(
  rte: RTE.ReaderTaskEither<D0, E0, T>,
) => RTE.ReaderTaskEither<D0 & D1 & D2, E0 | E1 | E2, T1 | T2>;
export function branchW<T, D1, D2, E1, E2, T1, T2>(
  predicate: (val: T) => boolean,
  onFalse: (val: T) => RTE.ReaderTaskEither<D1, E1, T1>,
  onTrue: (val: T) => RTE.ReaderTaskEither<D2, E2, T2>,
) {
  return <E0, D0>(
    rte: RTE.ReaderTaskEither<D0, E0, T>,
  ): RTE.ReaderTaskEither<D0 & D1 & D2, E0 | E1 | E2, T1 | T2> => {
    return pipe(
      rte,
      RTE.chainW((v) => {
        if (predicate(v)) {
          return onTrue(v) as RTE.ReaderTaskEither<D1 | D2, E1 | E2, T1 | T2>;
        }

        return onFalse(v) as RTE.ReaderTaskEither<D1 | D2, E1 | E2, T1 | T2>;
      }),
    );
  };
}

This fulfills our specific use case of ā€œI want to follow a different path of logic depending on a conditionā€ and ā€œI want to infer the return type so I don’t have to explicitly declare all errors and dependenciesā€

Read more comments on GitHub >

github_iconTop Results From Across the Web

Optional chaining does not narrow types in the `else` branch
Successfully merging a pull request may close this issue. Fix discriminant property narrowing through optional chain with null andrewbranch/Ā ...
Read more >
All branches in a conditional structure should not have exactly ...
Having all branches in a switch or if chain with the same implementation is an error. Either a copy-paste error was made and...
Read more >
Conditional type is not being narrowed by if/else branch
I tried inferring the type using typeof which narrows correctly for new declarations in the branch but does not narrow the callback.
Read more >
Documentation - Narrowing - TypeScript
In JavaScript, we can use any expression in conditionals, && s, || s, if statements, Boolean negations ( ! ), and more. As...
Read more >
Conditional Branch - an overview | ScienceDirect Topics
Boolean-valued comparisons do not help with the code in Figure 7.9a. The code is equivalent to the straight condition-code scheme. It requires comparisons,Ā ......
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found