Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OrElse callback should be able to change Ok type #484

Merged
merged 11 commits into from
Sep 8, 2024
19 changes: 19 additions & 0 deletions .changeset/empty-poets-collect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
'neverthrow': major
---

Allow orElse method to change ok types.
This makes the orElse types match the implementation.

This is a breaking change for the orElse type argument list,
as the ok type must now be provided before the err type.

```diff
- result.orElse<ErrType>(foo)
+ result.orElse<OkType, ErrType>(foo)
```

This only applies if type arguments were
explicitly provided at an orElse callsite.
If the type arguments were inferred,
no updates are needed during the upgrade.
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -407,9 +407,9 @@ Takes an `Err` value and maps it to a `Result<T, SomeNewType>`. This is useful f

```typescript
class Result<T, E> {
orElse<A>(
callback: (error: E) => Result<T, A>
): Result<T, A> { ... }
orElse<U, A>(
callback: (error: E) => Result<U, A>
): Result<U | T, A> { ... }
}
```

Expand Down Expand Up @@ -1179,9 +1179,9 @@ Takes an `Err` value and maps it to a `ResultAsync<T, SomeNewType>`. This is use

```typescript
class ResultAsync<T, E> {
orElse<A>(
callback: (error: E) => Result<T, A> | ResultAsync<T, A>
): ResultAsync<T, A> { ... }
orElse<U, A>(
callback: (error: E) => Result<U, A> | ResultAsync<U, A>
): ResultAsync<U | T, A> { ... }
}
```

Expand Down
10 changes: 7 additions & 3 deletions src/result-async.ts
Original file line number Diff line number Diff line change
Expand Up @@ -163,9 +163,13 @@ export class ResultAsync<T, E> implements PromiseLike<Result<T, E>> {
)
}

orElse<R extends Result<T, unknown>>(f: (e: E) => R): ResultAsync<T, InferErrTypes<R>>
orElse<R extends ResultAsync<T, unknown>>(f: (e: E) => R): ResultAsync<T, InferAsyncErrTypes<R>>
orElse<A>(f: (e: E) => Result<T, A> | ResultAsync<T, A>): ResultAsync<T, A>
orElse<R extends Result<unknown, unknown>>(
f: (e: E) => R,
): ResultAsync<InferOkTypes<R> | T, InferErrTypes<R>>
orElse<R extends ResultAsync<unknown, unknown>>(
f: (e: E) => R,
): ResultAsync<InferAsyncOkTypes<R> | T, InferAsyncErrTypes<R>>
orElse<U, A>(f: (e: E) => Result<U, A> | ResultAsync<U, A>): ResultAsync<U | T, A>
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
orElse(f: any): any {
return new ResultAsync(
Expand Down
18 changes: 12 additions & 6 deletions src/result.ts
Original file line number Diff line number Diff line change
Expand Up @@ -212,8 +212,10 @@ interface IResult<T, E> {
* @param f A function to apply to an `Err` value, leaving `Ok` values
* untouched.
*/
orElse<R extends Result<unknown, unknown>>(f: (e: E) => R): Result<T, InferErrTypes<R>>
orElse<A>(f: (e: E) => Result<T, A>): Result<T, A>
orElse<R extends Result<unknown, unknown>>(
f: (e: E) => R,
): Result<InferOkTypes<R> | T, InferErrTypes<R>>
orElse<U, A>(f: (e: E) => Result<U, A>): Result<U | T, A>

/**
* Similar to `map` Except you must return a new `Result`.
Expand Down Expand Up @@ -328,8 +330,10 @@ export class Ok<T, E> implements IResult<T, E> {
return ok<T, E>(this.value)
}

orElse<R extends Result<unknown, unknown>>(_f: (e: E) => R): Result<T, InferErrTypes<R>>
orElse<A>(_f: (e: E) => Result<T, A>): Result<T, A>
orElse<R extends Result<unknown, unknown>>(
_f: (e: E) => R,
): Result<InferOkTypes<R> | T, InferErrTypes<R>>
orElse<U, A>(_f: (e: E) => Result<U, A>): Result<U | T, A>
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
orElse(_f: any): any {
return ok(this.value)
Expand Down Expand Up @@ -416,8 +420,10 @@ export class Err<T, E> implements IResult<T, E> {
return err(this.error)
}

orElse<R extends Result<unknown, unknown>>(f: (e: E) => R): Result<T, InferErrTypes<R>>
orElse<A>(f: (e: E) => Result<T, A>): Result<T, A>
orElse<R extends Result<unknown, unknown>>(
f: (e: E) => R,
): Result<InferOkTypes<R> | T, InferErrTypes<R>>
orElse<U, A>(f: (e: E) => Result<U, A>): Result<U | T, A>
// eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types
orElse(f: any): any {
return f(this.error)
Expand Down
80 changes: 78 additions & 2 deletions tests/typecheck-tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -349,10 +349,48 @@ type CreateTuple<L, V = string> =
(function it(_ = 'allows specifying the E and T types explicitly') {
type Expectation = Result<'yo', string>

const result: Expectation = ok<'yo', number>('yo').orElse<string>(val => {
const result: Expectation = ok<'yo', number>('yo').orElse<'yo', string>(val => {
return err('yo')
})
});

(function it(_ = 'Creates a union of ok types for disjoint types') {
type Expectation = Result<string | number, boolean>

const result: Expectation = err<string, boolean[]>([true])
.orElse((val) => ok<string, boolean>('recovered!'))
});

(function it(_ = 'Infers ok type when returning disjoint types') {
type Expectation = Result<string | number | boolean, unknown>

const result: Expectation = err<string, number>(123)
.orElse((val) => {
switch (val) {
case 1:
return ok('yoooooo dude' + val)
case 2:
return ok(123)
default:
return ok(false)
}
})
});

(function it(_ = 'Infers new type when returning both Ok and Err') {
const initial = err<string, number>(123)
type Expectation = Result<string | true, false>

const result: Expectation = initial
.orElse((val) => {
switch (val) {
case 1:
return err(false as const)
default:
return ok(true as const)
}
})
});
});

(function describe(_ = 'match') {
Expand Down Expand Up @@ -1604,7 +1642,7 @@ type CreateTuple<L, V = string> =
type Expectation = ResultAsync<number, number | string>

const result: Expectation = okAsync<number, string>(123)
.orElse<number | string>((val) => {
.orElse<number, number | string>((val) => {
switch (val) {
case '1':
return ok(1)
Expand All @@ -1615,6 +1653,44 @@ type CreateTuple<L, V = string> =
}
})
});

(function it(_ = 'Creates a union of ok types for disjoint types') {
type Expectation = ResultAsync<string | number, boolean>

const result: Expectation = errAsync<string, boolean[]>([true])
.orElse((val) => ok<string, boolean>('recovered!'))
});

(function it(_ = 'Infers ok type when returning disjoint types') {
type Expectation = ResultAsync<string | number | boolean, unknown>

const result: Expectation = errAsync<string, number>(123)
.orElse((val) => {
switch (val) {
case 1:
return okAsync('yoooooo dude' + val)
case 2:
return okAsync(123)
default:
return okAsync(false)
}
})
});

(function it(_ = 'Infers new type when returning both Ok and Err') {
const initial = errAsync<string, number>(123)
type Expectation = ResultAsync<string | true, false>

const result: Expectation = initial
.orElse((val) => {
switch (val) {
case 1:
return err(false as const)
default:
return okAsync(true as const)
}
})
});
});

(function describe(_ = 'combine') {
Expand Down