Why does `ResultAsync` implement `PromiseLike` instead of `Promise`?
Josef37 opened this issue ยท 2 comments
This is a minor nit-pick that would allow me to simplify/unify some code ๐ค
Problem description
ResultAsync
isthenable
meaning it behaves exactly like a nativePromise<Result>
...
But using ResultAsync.then
doesn't return a Promise
which in turn doesn't even allow me to create a ResultAsync
from it...
const result = new ResultAsync(okAsync({}).then(x => x));
This fails with Argument of type 'PromiseLike<unknown>' is not assignable to parameter of type 'Promise<Result<unknown, unknown>>'
.
In some cases functions are easier to write using await
resulting in a Promise<Result>
and sometimes ResultAsync
is nicer.
Why is that important?
Is isn't really... It's just a nice-to-have. I'm fine with type-casting in a few places.
Code where problem occurred
I have written quite a black-magic function to simplify chaining functions which depend on results of previous steps in which I'd like to treat Promise<Result>
and ResultAsync
equally.
The comments should explain most of it:
/**
* Merges provided `args` with the success result of the `callback` allowing to easily chain callbacks and use results for earlier steps.
*
* Example:
* ```ts
* const first = preserveArgs(() => okAsync({ one: 2 }));
* const second = preserveArgs(() => okAsync({ two: 3 }));
* const third = preserveArgs(({ one, two }) => okAsync({ three: one + two }));
*
* const result = okAsync({})
* .andThen(first) // `{}` -> `{ one: 2 }`
* .andThen(second) // `{ one: 2 }` -> `{ one: 2, two: 3 }`
* .andThen(third); // `{ one: 2, two: 3 }` -> `{ one: 2, two: 3, three: 5 }`
* ```
*/
export const preserveArgs = <CallbackArgs, Ok, Err>(callback: (args: CallbackArgs) => Promise<Result<Ok, Err>> | ResultAsync<Ok, Err>) =>
<Args extends CallbackArgs>(args: Args) => {
const asyncResult = callback(args);
const promise = asyncResult.then((result) =>
result.map((success) => ({ ...args, ...success })),
);
// We need to type-cast here, because `ResultAsync.then` returns a `PromiseLike` object,
// but expects a `Promise` object in the constructor.
// In reality, `ResultAsync.then` always returns a `Promise` object.
return new ResultAsync(promise as Promise<Result<Args & Ok, Err>>);
};
Formulating the problem helped me figure out a nice solution for my use-case.
Still, I'll leave the issue up for discussion ๐
type AnyResult<Ok, Err> =
| Result<Ok, Err>
| Promise<Result<Ok, Err>>
| ResultAsync<Ok, Err>;
const makeResultAsync = <Ok, Err>(result: AnyResult<Ok, Err>) =>
result instanceof ResultAsync
? result
: new ResultAsync(Promise.resolve(result));
/** ... */
export const preserveArgs = <CallbackArgs, Ok, Err>(callback: (args: CallbackArgs) => AnyResult<Ok, Err>) =>
<Args extends CallbackArgs>(args: Args) => {
const result = callback(args);
const resultAsync = makeResultAsync(result);
return resultAsync.map((success) => ({ ...args, ...success }));
};
Hmmm, interesting. It was overlooked to update the definition of the ResultAsync
constructor to accept a PromiseLike
rather than a promise. That being said, I don't see myself pushing this change as it has downstream implications that I don't think justify implementing this change:
- constructor(res: Promise<Result<T, E>>) {
+ constructor(res: PromiseLike<Result<T, E>>) {
this._promise = res
}
That being said, it's not advised to use the constructor - but rather use the from*
apis:
.fromPromise
.fromSafePromise
Example:
Here's your code using the ideomatic fromSafePromise
/ fromPromise
API:
export const preserveArgs = <CallbackArgs, Ok, Err>(callback: (args: CallbackArgs) => Promise<Result<Ok, Err>> | ResultAsync<Ok, Err>) =>
<Args extends CallbackArgs>(args: Args) => {
const asyncResult = callback(args);
const promise = asyncResult.then((result) =>
result.map((success) => ({ ...args, ...success })),
);
// Ideomatic `neverthrow` usage :)
// ResultAsync.fromSafePromise(promise);
// ResultAsync.fromPromise(promise, errorHandlerFn);
//
return ResultAsync.fromSafePromise(promise);
};
Some context
To answer your original question, "Why does ResultAsync implement PromiseLike instead of Promise?". The reason is that this is the protocol in order to make things "thenable".
By indicating ResultAsync<T, E> implements PromiseLike<Result<T, E>> {}
we are forced to then define how .then
/ async / await
behaviour works for a ResultAsync
.
https://github.com/supermacro/neverthrow/blob/master/src/result-async.ts#L141
Conclusion / Summary
Please do not use new ResultAsync
, instead use the ResultAsync.fromPromise
& ResultAsync.fromSafePromise
apis