supermacro / neverthrow

Type-Safe Errors for JS & TypeScript
MIT License
4.06k stars 85 forks source link

More ergonomic `match` #497

Closed braxtonhall closed 3 months ago

braxtonhall commented 1 year ago

When writing a match in neverthrow, the ok and err callbacks must have the same ReturnType in their signature. This means the following code results in an error

declare const result: Result<string, number>
const matchResult = result.match(
    (value: string) => !!value,
    (value: number) => BigInt(10),
//  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// TS2345: Argument of type  (value: number) => bigint  is not assignable to parameter of type  (e: number) => boolean 
// Type  bigint  is not assignable to type  boolean
)

This can be fixed by annotating the callbacks or the match application with more generic types

declare const result: Result<string, number>
const matchResultA = result.match(
    (value: string): boolean | bigint => !!value,
    (value: number) => BigInt(10),
)
const matchResultB = result.match<boolean | bigint>(
    (value: string) => !!value,
    (value: number) => BigInt(10),
)

However this is pretty inconvenient!

Instead, match can infer that the result is the union of whatever ok returns, and whatever err returns

  interface Result<T, E> {
    // ...
-   match<A>(ok: (t: T) => A, err: (e: E) => A): A;
+   match<A, B>(ok: (t: T) => A, err: (e: E) => B): A | B;
    // ...
  }
declare const result: Result<string, number>
const matchResult = result.match(
    (value: string) => !!value,
    (value: number) => BigInt(10),
)

true satisfies typeof matchResult
BigInt(10) satisfies typeof matchResult

Unfortunately, this would be a breaking change! Anyone who had already annotated their method call would now get an error

declare const result: Result<string, number>
const matchResult = result.match<boolean | bigint>(
//                               ~~~~~~~~~~~~~~~~
// TS2558: Expected  2  type arguments, but got  1 
    (value: string) => !!value,
    (value: number) => BigInt(10),
)

To support legacy code, we can add a default to the second type argument

  interface Result<T, E> {
    // ...
-   match<A>(ok: (t: T) => A, err: (e: E) => A): A;
+   match<A, B = A>(ok: (t: T) => A, err: (e: E) => B): A | B;
    // ...
  }
changeset-bot[bot] commented 4 months ago

🦋 Changeset detected

Latest commit: 1f87908f15d387c539281bfc584625a15dbd9f21

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package | Name | Type | | ---------- | ----- | | neverthrow | Patch |

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR