title: Either monad in TypeScript + pattern matching
body: <p>I'm trying to implement Either monad in TypeScript.</p>
  
  <pre><code>interface Either&lt;TResult, TError&gt; {
  
    flatMap&lt;R&gt;(f: (value: TResult) =&gt; Either&lt;R, TError&gt;): Either&lt;R, TError&gt;;
  
    match&lt;R&gt;(result: (success: TResult) =&gt; R, error: (err: TError) =&gt; R): R;
  }
  
  class Left&lt;TResult, TError&gt; implements Either&lt;TResult, TError&gt; {
  
    public constructor(private readonly error: TError) {}
  
    public flatMap&lt;R&gt;(f: (value: TResult) =&gt; Either&lt;R, TError&gt;): Either&lt;R, TError&gt; {
  
      return new Left(this.error);
    }
  
    public match&lt;R&gt;(success: (value: TResult) =&gt; R, error: (err: TError) =&gt; R): R {
      return error(this.error);
    }
  }
  
  class Right&lt;TResult, TError&gt; implements Either&lt;TResult, TError&gt; {
  
    public constructor(private readonly value: TResult) {}
  
    public flatMap&lt;R&gt;(f: (value: TResult) =&gt; Either&lt;R, TError&gt;): Either&lt;R, TError&gt; {
  
      return f(this.value);
    }
  
    public match&lt;R&gt;(success: (result: TResult) =&gt; R, error: (err: TError) =&gt; R): R {
      return success(this.value);
    }
  }
  
  class ResourceError {
  }
  
  class ObjectNotFound  {
  
    public constructor(public readonly msg: string, public readonly code: number) {
    }
  }
  
  function f1(s: string): Either&lt;number, ObjectNotFound&gt; {
    return new Right(+s);
  }
  
  function f2(n: number): Either&lt;number, ResourceError&gt; {
    return new Right(n + 1);
  }
  
  function f3(n: string): Either&lt;string, ObjectNotFound&gt; {
    return new Right(n.toString());
  }
  
  const c = f1('345') 
  // (*) line 58 - error: Type 'Either&lt;number, ResourceError&gt;' is not assignable to type 'Either&lt;number, ObjectNotFound&gt;'. 
    .flatMap(n =&gt; f2(n)) 
    .flatMap(n =&gt; f3(n));
  
  const r = c.match(
    (result: string) =&gt; result,
    (err: ObjectNotFound) =&gt; err.msg
  );
  </code></pre>
  
  <p>Link to <a href="https://www.typescriptlang.org/play/index.html?ssl=1&amp;ssc=1&amp;pln=64&amp;pc=3#code/JYOwLgpgTgZghgYwgAgKLDAC2gHgCoBKEAzgK4A2YANMnqlFAPZQB8yA3gLABQPyyMcnDABZOAAccBFgAoYALmQyAbnHKkIiwiQpgAlMgC8bdFlwEadBsxZ7Fp7FCmX6TVgG4efZAFthCTClZKB1KRRkyBCRiYi0iMkoDY2QLZGg3cPStV2YktgI7FM9uAF8vbgQhGOQAGQgYMHx43RdrVmRgH3FyCB8IcGI0DEcm0OpaHPauXm5+cVIAI3JgBGQERhBiMChSBDBmGXEoYFVIZBC4ABMN8gBPNLbstoN2Mpm5xeXVwWExSWk5OFVOpNLRmokjCZhuZWm5bPZoU5UlY4Rxyvx+CEwKQoCBkCAIAB3Wr1MAyLDAYgAOnSuWK-De3nmSxWvn8gQBkWisSUwI0cTGeRSNFpUEyDCebiFBUUBDRswxWJxeNF5MwlJpz3pyDejIqVUGBGAAHNMI1tAlxiibB0uj0+gMhmYnBaWhM2mxpkzPqz1pttrt9lBDsdTigLtcQHdkHzQa6Ia90chmV8BEJRBIgoDeWp+WDBZCnSNkZN4UWYe7UV6FZiINjcQI1RrY3ptXqPizVn4wAEs1ySDyZCFLQLLdKRY8lFlK7lCzKUvKMec68rkP2Yk3qS22zw9ZU4NV4owcUhJvK9wbkAB5BYAKwgewAcowwAAxY8gS78asd1N+rY7HsBwpqyEY3PcPjEMaigAaAxo0CBqxgVG9zrJcoIgKQPgLNALzeLq5QwKQIB7MAGwCAAjBEMHbHBhQOLgmHYdANA3veT4vu+xGXJ63hKg2BLEkappkgA1MQra7oRxGkeRMAAEwyCAihMThUD0YiOCqSxKQkMeUCnh6i7LvWeKCSkJpmkpyCicgFGSaU0kkWAZF4jAADMSk0ccIDGhpzo4LBvmsXeD5gM+b4fjxxn8WZRIWSJSlUvsADKtG+TIegOXuGxbGsRiUTIADk7kACwAKxFXo3hUj8GbiNZyQKUpWU1XVfyNWwHktQ5PD-mA5wFQgVLdgEMjeEOYzeXRhbDi0E3TmxYURVxn5CukI1QTwrZAA" rel="nofollow noreferrer">playground</a>.</p>
  
  <p>As different functions can potentially generate errors of different types, flatMap chaining breaks.</p>
  
  <p>I assume, overall intention is clear from the code. And I hope I've chosen the right tool (either monad).</p>
  
  <p>Can anyone suggest a fix to make it all work?</p>
  
  <p><strong>[UPDATE]:</strong> Thanks to <a href="https://stackoverflow.com/users/10609635/spencerpark">SpencerPark</a> for the nudge in right direction.
  The full implementation with .match working as originally intended is below (inspired by <a href="https://medium.com/@fillopeter/pattern-matching-with-typescript-done-right-94049ddd671c" rel="nofollow noreferrer">this</a> post):</p>
  
  <pre><code>type UnionOfNames&lt;TUnion&gt; = TUnion extends { type: any } ? TUnion["type"] : never
  
  type UnionToMap&lt;TUnion&gt; = {
    [NAME in UnionOfNames&lt;TUnion&gt;]: TUnion extends { type: NAME } ? TUnion : never
  }
  
  type Pattern&lt;TResult, TMap&gt; = {
    [NAME in keyof TMap]: (value: TMap[NAME]) =&gt; TResult;
  }
  
  type Matcher&lt;TUnion, TResult&gt; = Pattern&lt;TResult, UnionToMap&lt;TUnion&gt;&gt;;
  
  
  interface Either&lt;TResult, TError&gt; {
  
    flatMap&lt;R, E&gt;(f: (value: TResult) =&gt; Either&lt;R, E&gt;): Either&lt;R, E | TError&gt;;
  
    match&lt;R&gt;(success: (result: TResult) =&gt; R, error: Matcher&lt;TError, R&gt;): R;
  }
  
  class Left&lt;TResult, TError extends { type: any }&gt; implements Either&lt;TResult, TError&gt; {
  
    public constructor(private readonly error: TError) {}
  
    public flatMap&lt;R, E&gt;(f: (value: TResult) =&gt; Either&lt;R, E&gt;): Either&lt;R, E | TError&gt; {
      return new Left(this.error);
    }
  
    public match&lt;R&gt;(success: (result: TResult) =&gt; R, error: Matcher&lt;TError, R&gt;): R {
      return (error as any)[this.error.type](this.error);
    }
  }
  
  class Right&lt;TResult, TError&gt; implements Either&lt;TResult, TError&gt; {
  
    public constructor(private readonly value: TResult) {}
  
    public flatMap&lt;R, E&gt;(f: (value: TResult) =&gt; Either&lt;R, E&gt;): Either&lt;R, E | TError&gt; {
      return f(this.value);
    }
  
    match&lt;R&gt;(success: (result: TResult) =&gt; R, error: Matcher&lt;TError, R&gt;): R {
      return success(this.value);
    }
  }
  
  class ResourceError {
    type = "ResourceError" as const
  
    public constructor(public readonly resourceId: number) {}
  }
  
  class ObjectNotFound {
    type = "ObjectNotFound" as const
  
    public constructor(public readonly msg: string, public readonly timestamp: string) {}
  }
  
  class DivisionByZero {
    type = "DivisionByZero" as const
  }
  
  class NetworkError {
    type = "NetworkError" as const
  
    public constructor(public readonly address: string) {}
  }
  
  
  function f1(s: string): Either&lt;number, DivisionByZero&gt; {
    return new Right(+s);
  }
  
  function f2(n: number): Either&lt;number, ResourceError&gt; {
    return new Right(n + 1);
  }
  
  function f3(n: number): Either&lt;string, ObjectNotFound&gt; {
    return new Right(n.toString());
  }
  
  function f4(s: string): Either&lt;number, NetworkError&gt; {
    return new Left(new NetworkError('someAdress'));
  }
  
  const c = f1('345')
    .flatMap(n =&gt; f2(n))
    .flatMap(n =&gt; f3(n))
    .flatMap(s =&gt; f4(s));
  
  const r = c.match(
    (result: number) =&gt; result.toString(),
    {
      ObjectNotFound: (value: ObjectNotFound) =&gt; 'objectNotFound',
      ResourceError: (value: ResourceError) =&gt; 'resourceError',
      DivisionByZero: (value: DivisionByZero) =&gt; 'divisionByZero',
      NetworkError: (value: NetworkError) =&gt; value.address
    }
  );
  
  console.log(r);
  </code></pre>
  
  <p>Updated playground is <a href="https://www.typescriptlang.org/play/index.html?ssl=1&amp;ssc=1&amp;pln=101&amp;pc=16#code/C4TwDgpgBAqgdgSwPZwPIDMByBDAthAZwB4AVeZOAPigF4ozEUoIAPYCOAEwKgG8pQkAFxRscEFAC+UAPz1yKANoAiQRGUBdKCLgQAbhABOAWABQZtbEZwSSALLYwpBVVp8zUKIswBBOwFEoBDgrCgwcfGIGCkoNEWimVnYuHn41EV8AqVl5a20oXQMTU0kzC3BoAAVsYHZDOFIAJUIAVwAbYAAaegcwajpeDy9MwOCoAGsIECR0Hsc4qAAKPWw2loh43u8-fw0ASlpqEmaCduAAbjNS81NLB2AAYwALI2drbuPWjv6oatqjBqfU4dbouWy9N4xSiXG5mYJ1dDYB7QfwIYAvQxNL5dej+QyGJCGaiDG6edBtGoQxrdfyURboETLVbreInM4HGjUVHo17UqC0vYibkYoh8wIAH1x+MJ0LKpk8uBqz1FdNOD2RBAIjMM2NZ2I51D5RgJhhE92erxIeJN3UalEFUEaMOuZgeFM1UAAMhB0MAscCcVbpYZmGwONw+AIKiIxBJJNQELgwG0IPg4MAeMLLWyQVKTcS5Z4wC0AEZtBAPKAPFAEYCGFoPYCExZgQwIFbsKA67CcFBtCTGwnxa2Eg68F3yqDFssVqDkymOUU0ukMpYrNYbeg54AG-lokVi+1C-e8mlQSVB-PuSeeHXAFr1AoQADuXp9wEW6IQBAAdIPDHsMKeBORaluWlaKo8TwqosaoalqSw6gGeoBruRrBmaSoipehK2kejrXp4t4QPej6LP+og8LGeyKF+v7-j+agaJ+Tzfn+waAUM1wTm62Aeo0CAAOZPH6QJnB8I5EkESYpmmGZ7jymJibmOFSSSQzTuBVY1nWDZNoYLZth20Ddr2cD9lA64slu+p8CBU5gbO87AFSy70oyVmbspO6HApB7Lg6WaYmK555jKhFEXeD4hOgLFsZ5nGTvZkHKnasENvB2q6jZqG+ehJqYVBlqSXhDqNBFxGkSEcGEAQcW-glQFSFccq8fxhBIA+yKSRFlh0MoJydYY3XBsolHaXAtaFg5M6VtWk26Y2zaabOpl9hISFDciACSnA6C0uAlkYY7ca17o8KgJYAFYQI2mBIMAABinVcL1FRuMol03XdD3PS0XBjXxE1TaSM1afNtb1ktBkrZWa3mRIuAEIJIiQ8EgndLDXYQD260CImhDAHgYCo3W6MnS1NxtTwAAi7bfhQABCIAAFpGEgb2QB9dN6AzKDM2zBKAzwEPAJTrrnVAmAkc+hLjD1JKeH1UDKNLwCy4Y8ujeNovTVjotQ-pLaOXDONmRZPacEhCFo3AgkUyUcpmOg-2NhQc4AIywaTbZ24FJ6YnAB1HYY3Q83zcAC+zBaTlFj66K+AnCR+ADUBCJROLtwG7TDoAATIscD7Ydx3HopRBByXoeOh1XUQJJMeVdFT6J0JImF1AKdQB7GdylnOcxQAzIXxch-75e2xjUBfbdwD3U9L2cI32NVS3jptx+cCMUgADKZN24sey9zc-fAO76AACze1Ak-jyKlch90asa1rV6KyvzcJ2+vqFy+Usy3LSSiwADkBAkD4B8FbWqwCj7OlajpKsbh0Be2AYPC+ABWGBQwfzOV6B3Tkc4C5wCPtg3Bjh8HUHQMPYhexSEUhcuQngBDL6wVgfAhaXY3APB-ClJ4iwhiLCQmcUex1fJCI6NvPevtBKH06EMd+ngZ4-QXv9Paa5mSbiUXPX6i9dzAKQNdWe88-pcGAXIm8NcwF10kh5DRIhBrWI4r5YBm1HEmjMUMTw4cCBM1ZuzWxG4RDeN8YLJAejOD0x8fzPxBIPEWOfoAjC6jAn-3Vokk0u5PI-kttbLiZhEquhrEgFMP42hIBkQBc4QA" rel="nofollow noreferrer">here</a>.</p>
  
  <p><strong>[UDPDATE 2]:</strong>
  'catchall' clause support:</p>
  
  <pre><code>type DiscriminatedUnion&lt;T&gt; = { type: T }
  
  type UnionOfNames&lt;TUnion&gt; = TUnion extends DiscriminatedUnion&lt;string&gt; ? TUnion["type"] : never
  
  type UnionToMap&lt;TUnion&gt; = {
    [NAME in UnionOfNames&lt;TUnion&gt;]: TUnion extends DiscriminatedUnion&lt;NAME&gt; ? TUnion : never
  }
  
  type Pattern&lt;TResult, TMap&gt; = {
    [NAME in keyof TMap]: (value: TMap[NAME]) =&gt; TResult
  } | ({
    [NAME in keyof TMap]?: (value: TMap[NAME]) =&gt; TResult
  } &amp; { catchall: (value: DiscriminatedUnion&lt;string&gt;) =&gt; TResult})
  
  // ... SKIPPED ...
  
  const r1 = c.match(
    (result: string) =&gt; result,
    {
      // when there is the 'catchall' case, others are optional. But still only the possible cases can be listed.
      ObjectNotFound: (value: ObjectNotFound) =&gt; value.msg + value.timestamp,
      catchall: (value: DiscriminatedElement&lt;string&gt;) =&gt; 'catchall ' + value.type
    }
  );
  </code></pre>