fix(util/result): Types for wrapNullable (#23713)

This commit is contained in:
Sergei Zharinov 2023-08-04 18:00:11 +03:00 committed by GitHub
parent 77952db8d9
commit 8c0013f1fc
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 178 additions and 131 deletions

View file

@ -345,7 +345,7 @@ export function getRawPkgReleases(
return AsyncResult.err('no-package-name');
}
return Result.wrapNullable(getRawReleases(config), 'no-result')
return Result.wrapNullable(getRawReleases(config), 'no-result' as const)
.catch((e) => {
if (e instanceof ExternalHostError) {
e.hostType = config.datasource;

View file

@ -13,10 +13,10 @@ import { MetadataCache } from './metadata-cache';
import { GemInfo, MarshalledVersionInfo } from './schema';
import { VersionsEndpointCache } from './versions-endpoint-cache';
function unlessServerSide<T, E>(
err: E,
cb: () => AsyncResult<T, E>
): AsyncResult<T, E> {
function unlessServerSide<
T extends NonNullable<unknown>,
E extends NonNullable<unknown>
>(err: E, cb: () => AsyncResult<T, E>): AsyncResult<T, E> {
if (err instanceof HttpError && err.response?.statusCode) {
const code = err.response.statusCode;
if (code >= 500 && code <= 599) {

View file

@ -23,14 +23,14 @@ export class MetadataCache {
const cacheKey = `metadata-cache:${registryUrl}:${packageName}`;
const hash = toSha256(versions.join(''));
const loadCache = (): AsyncResult<ReleaseResult, unknown> =>
const loadCache = (): AsyncResult<ReleaseResult, NonNullable<unknown>> =>
Result.wrapNullable(
packageCache.get<CacheRecord>(cacheNs, cacheKey),
'cache-not-found'
'cache-not-found' as const
).transform((cache) => {
return hash === cache.hash
? Result.ok(cache.data)
: Result.err('cache-outdated');
: Result.err('cache-outdated' as const);
});
const saveCache = async (data: ReleaseResult): Promise<ReleaseResult> => {

View file

@ -40,7 +40,10 @@ describe('util/result', () => {
});
it('wraps nullable callback', () => {
const res = Result.wrapNullable(() => 42, 'oops');
const res: Result<number, 'oops'> = Result.wrapNullable(
(): number | null => 42,
'oops'
);
expect(res).toEqual(Result.ok(42));
});
@ -225,7 +228,10 @@ describe('util/result', () => {
});
it('wraps nullable promise', async () => {
const res = Result.wrapNullable(Promise.resolve(42), 'oops');
const res: AsyncResult<number, 'oops'> = Result.wrapNullable(
Promise.resolve<number | null>(42),
'oops'
);
await expect(res).resolves.toEqual(Result.ok(42));
});

View file

@ -1,15 +1,18 @@
import { SafeParseReturnType, ZodError } from 'zod';
import { logger } from '../logger';
interface Ok<T> {
type Val = NonNullable<unknown>;
type Nullable<T extends Val> = T | null | undefined;
interface Ok<T extends Val> {
readonly ok: true;
readonly val: NonNullable<T>;
readonly val: T;
readonly err?: never;
}
interface Err<E> {
interface Err<E extends Val> {
readonly ok: false;
readonly err: NonNullable<E>;
readonly err: E;
readonly val?: never;
/**
@ -19,11 +22,11 @@ interface Err<E> {
readonly _uncaught?: true;
}
type Res<T, E> = Ok<T> | Err<E>;
type Res<T extends Val, E extends Val> = Ok<T> | Err<E>;
function isZodResult<Input, Output>(
function isZodResult<Input, Output extends Val>(
input: unknown
): input is SafeParseReturnType<Input, NonNullable<Output>> {
): input is SafeParseReturnType<Input, Output> {
if (
typeof input !== 'object' ||
input === null ||
@ -45,9 +48,9 @@ function isZodResult<Input, Output>(
}
}
function fromZodResult<Input, Output>(
input: SafeParseReturnType<Input, NonNullable<Output>>
): Result<Output, ZodError<Input>> {
function fromZodResult<ZodInput, ZodOutput extends Val>(
input: SafeParseReturnType<ZodInput, ZodOutput>
): Result<ZodOutput, ZodError<ZodInput>> {
return input.success ? Result.ok(input.data) : Result.err(input.error);
}
@ -55,9 +58,9 @@ function fromZodResult<Input, Output>(
* All non-nullable values that also are not Promises nor Zod results.
* It's useful for restricting Zod results to not return `null` or `undefined`.
*/
type RawValue<T> = Exclude<
NonNullable<T>,
SafeParseReturnType<unknown, NonNullable<T>> | Promise<unknown>
type RawValue<T extends Val> = Exclude<
T,
SafeParseReturnType<unknown, T> | Promise<unknown>
>;
/**
@ -68,18 +71,18 @@ type RawValue<T> = Exclude<
* - `.transform()` are pipes which can be chained
* - `.unwrap()` is the point of consumption
*/
export class Result<T, E = Error> {
export class Result<T extends Val, E extends Val = Error> {
private constructor(private readonly res: Res<T, E>) {}
static ok<T>(val: NonNullable<T>): Result<T, never> {
static ok<T extends Val>(val: T): Result<T, never> {
return new Result({ ok: true, val });
}
static err<E>(err: NonNullable<E>): Result<never, E> {
static err<E extends Val>(err: E): Result<never, E> {
return new Result({ ok: false, err });
}
static _uncaught<E>(err: NonNullable<E>): Result<never, E> {
static _uncaught<E extends Val>(err: E): Result<never, E> {
return new Result({ ok: false, err, _uncaught: true });
}
@ -112,17 +115,26 @@ export class Result<T, E = Error> {
*
* ```
*/
static wrap<T, Input = any>(
zodResult: SafeParseReturnType<Input, NonNullable<T>>
static wrap<T extends Val, Input = any>(
zodResult: SafeParseReturnType<Input, T>
): Result<T, ZodError<Input>>;
static wrap<T, E = Error>(callback: () => RawValue<T>): Result<T, E>;
static wrap<T, E = Error, EE = never>(
static wrap<T extends Val, E extends Val = Error>(
callback: () => RawValue<T>
): Result<T, E>;
static wrap<T extends Val, E extends Val = Error, EE extends Val = never>(
promise: Promise<Result<T, EE>>
): AsyncResult<T, E | EE>;
static wrap<T, E = Error>(promise: Promise<RawValue<T>>): AsyncResult<T, E>;
static wrap<T, E = Error, EE = never, Input = any>(
static wrap<T extends Val, E extends Val = Error>(
promise: Promise<RawValue<T>>
): AsyncResult<T, E>;
static wrap<
T extends Val,
E extends Val = Error,
EE extends Val = never,
Input = any
>(
input:
| SafeParseReturnType<Input, NonNullable<T>>
| SafeParseReturnType<Input, T>
| (() => RawValue<T>)
| Promise<Result<T, EE>>
| Promise<RawValue<T>>
@ -151,7 +163,7 @@ export class Result<T, E = Error> {
* hence never re-thrown.
*
* Since functions and promises returning nullable can't be wrapped with `Result.wrap()`
* because `val` is constrained by being `NonNullable<T>`, `null` and `undefined`
* because `val` is constrained by being `NonNullable`, `null` and `undefined`
* must be converted to some sort of `err` value.
*
* This method does exactly this, i.g. it is the feature-rich shorthand for:
@ -187,47 +199,70 @@ export class Result<T, E = Error> {
*
* ```
*/
static wrapNullable<T, E = Error, NullableError = Error>(
callback: () => T,
nullableError: NonNullable<NullableError>
): Result<T, E | NullableError>;
static wrapNullable<T, E = Error, NullError = Error, UndefinedError = Error>(
callback: () => T,
nullError: NonNullable<NullError>,
undefinedError: NonNullable<UndefinedError>
): Result<T, E | NullError | UndefinedError>;
static wrapNullable<T, E = Error, NullableError = Error>(
promise: Promise<T>,
nullableError: NonNullable<NullableError>
): AsyncResult<T, E | NullableError>;
static wrapNullable<T, E = Error, NullError = Error, UndefinedError = Error>(
promise: Promise<T>,
nullError: NonNullable<NullError>,
undefinedError: NonNullable<UndefinedError>
): AsyncResult<T, E | NullError | UndefinedError>;
static wrapNullable<T, E = Error, NullError = Error, UndefinedError = Error>(
input: (() => T) | Promise<T>,
arg2: NonNullable<NullError>,
arg3?: NonNullable<UndefinedError>
static wrapNullable<
T extends Val,
E extends Val = Error,
ErrForNullable extends Val = Error
>(
callback: () => Nullable<T>,
errForNullable: ErrForNullable
): Result<T, E | ErrForNullable>;
static wrapNullable<
T extends Val,
E extends Val = Error,
ErrForNull extends Val = Error,
ErrForUndefined extends Val = Error
>(
callback: () => Nullable<T>,
errForNull: ErrForNull,
errForUndefined: ErrForUndefined
): Result<T, E | ErrForNull | ErrForUndefined>;
static wrapNullable<
T extends Val,
E extends Val = Error,
ErrForNullable extends Val = Error
>(
promise: Promise<Nullable<T>>,
errForNullable: ErrForNullable
): AsyncResult<T, E | ErrForNullable>;
static wrapNullable<
T extends Val,
E extends Val = Error,
ErrForNull extends Val = Error,
ErrForUndefined extends Val = Error
>(
promise: Promise<Nullable<T>>,
errForNull: ErrForNull,
errForUndefined: ErrForUndefined
): AsyncResult<T, E | ErrForNull | ErrForUndefined>;
static wrapNullable<
T extends Val,
E extends Val = Error,
ErrForNull extends Val = Error,
ErrForUndefined extends Val = Error
>(
input: (() => Nullable<T>) | Promise<Nullable<T>>,
arg2: ErrForNull,
arg3?: ErrForUndefined
):
| Result<T, E | NullError | UndefinedError>
| AsyncResult<T, E | NullError | UndefinedError> {
const nullError = arg2;
const undefinedError = arg3 ?? arg2;
| Result<T, E | ErrForNull | ErrForUndefined>
| AsyncResult<T, E | ErrForNull | ErrForUndefined> {
const errForNull = arg2;
const errForUndefined = arg3 ?? arg2;
if (input instanceof Promise) {
return AsyncResult.wrapNullable(input, nullError, undefinedError);
return AsyncResult.wrapNullable(input, errForNull, errForUndefined);
}
try {
const result = input();
if (result === null) {
return Result.err(nullError);
return Result.err(errForNull);
}
if (result === undefined) {
return Result.err(undefinedError);
return Result.err(errForUndefined);
}
return Result.ok(result);
@ -255,8 +290,8 @@ export class Result<T, E = Error> {
* ```
*/
unwrap(): Res<T, E>;
unwrap(fallback: NonNullable<T>): NonNullable<T>;
unwrap(fallback?: NonNullable<T>): Res<T, E> | NonNullable<T> {
unwrap(fallback: T): T;
unwrap(fallback?: T): Res<T, E> | T {
if (this.res.ok) {
return fallback === undefined ? this.res : this.res.val;
}
@ -275,7 +310,7 @@ export class Result<T, E = Error> {
/**
* Returns the ok-value or throw the error.
*/
unwrapOrThrow(): NonNullable<T> {
unwrapOrThrow(): T {
if (this.res.ok) {
return this.res.val;
}
@ -309,30 +344,28 @@ export class Result<T, E = Error> {
*
* ```
*/
transform<U, EE>(
fn: (value: NonNullable<T>) => Result<U, E | EE>
transform<U extends Val, EE extends Val>(
fn: (value: T) => Result<U, E | EE>
): Result<U, E | EE>;
transform<U, EE>(
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
transform<U extends Val, EE extends Val>(
fn: (value: T) => AsyncResult<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U, Input = any>(
fn: (value: NonNullable<T>) => SafeParseReturnType<Input, NonNullable<U>>
transform<U extends Val, Input = any>(
fn: (value: T) => SafeParseReturnType<Input, NonNullable<U>>
): Result<U, E | ZodError<Input>>;
transform<U, Input = any>(
fn: (
value: NonNullable<T>
) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
transform<U extends Val, Input = any>(
fn: (value: T) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
): AsyncResult<U, E | ZodError<Input>>;
transform<U, EE>(
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
transform<U extends Val, EE extends Val>(
fn: (value: T) => Promise<Result<U, E | EE>>
): AsyncResult<U, E | EE>;
transform<U>(
fn: (value: NonNullable<T>) => Promise<RawValue<U>>
transform<U extends Val>(
fn: (value: T) => Promise<RawValue<U>>
): AsyncResult<U, E>;
transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): Result<U, E>;
transform<U, EE, Input = any>(
transform<U extends Val>(fn: (value: T) => RawValue<U>): Result<U, E>;
transform<U extends Val, EE extends Val, Input = any>(
fn: (
value: NonNullable<T>
value: T
) =>
| Result<U, E | EE>
| AsyncResult<U, E | EE>
@ -377,18 +410,18 @@ export class Result<T, E = Error> {
}
}
catch<U = T, EE = E>(
fn: (err: NonNullable<E>) => Result<U, E | EE>
catch<U extends Val = T, EE extends Val = E>(
fn: (err: E) => Result<U, E | EE>
): Result<T | U, E | EE>;
catch<U = T, EE = E>(
fn: (err: NonNullable<E>) => AsyncResult<U, E | EE>
catch<U extends Val = T, EE extends Val = E>(
fn: (err: E) => AsyncResult<U, E | EE>
): AsyncResult<T | U, E | EE>;
catch<U = T, EE = E>(
fn: (err: NonNullable<E>) => Promise<Result<U, E | EE>>
catch<U extends Val = T, EE extends Val = E>(
fn: (err: E) => Promise<Result<U, E | EE>>
): AsyncResult<T | U, E | EE>;
catch<U = T, EE = E>(
catch<U extends Val = T, EE extends Val = E>(
fn: (
err: NonNullable<E>
err: E
) => Result<U, E | EE> | AsyncResult<U, E | EE> | Promise<Result<U, E | EE>>
): Result<T | U, E | EE> | AsyncResult<T | U, E | EE> {
if (this.res.ok) {
@ -426,7 +459,9 @@ export class Result<T, E = Error> {
*
* All the methods resemble `Result` methods, but work asynchronously.
*/
export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
export class AsyncResult<T extends Val, E extends Val>
implements PromiseLike<Result<T, E>>
{
private constructor(private asyncResult: Promise<Result<T, E>>) {}
then<TResult1 = Result<T, E>>(
@ -438,18 +473,23 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
return this.asyncResult.then(onfulfilled);
}
static ok<T>(val: NonNullable<T>): AsyncResult<T, never> {
static ok<T extends Val>(val: T): AsyncResult<T, never> {
return new AsyncResult(Promise.resolve(Result.ok(val)));
}
static err<E>(err: NonNullable<E>): AsyncResult<never, E> {
static err<E extends Val>(err: NonNullable<E>): AsyncResult<never, E> {
// eslint-disable-next-line promise/no-promise-in-callback
return new AsyncResult(Promise.resolve(Result.err(err)));
}
static wrap<T, E = Error, EE = never, Input = any>(
static wrap<
T extends Val,
E extends Val = Error,
EE extends Val = never,
Input = any
>(
promise:
| Promise<SafeParseReturnType<Input, NonNullable<T>>>
| Promise<SafeParseReturnType<Input, T>>
| Promise<Result<T, EE>>
| Promise<RawValue<T>>,
onErr?: (err: NonNullable<E>) => Result<T, E>
@ -476,20 +516,25 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
);
}
static wrapNullable<T, E, NullError, UndefinedError>(
promise: Promise<T>,
nullError: NonNullable<NullError>,
undefinedError: NonNullable<UndefinedError>
): AsyncResult<T, E | NullError | UndefinedError> {
static wrapNullable<
T extends Val,
E extends Val,
ErrForNull extends Val,
ErrForUndefined extends Val
>(
promise: Promise<Nullable<T>>,
errForNull: NonNullable<ErrForNull>,
errForUndefined: NonNullable<ErrForUndefined>
): AsyncResult<T, E | ErrForNull | ErrForUndefined> {
return new AsyncResult(
promise
.then((value) => {
if (value === null) {
return Result.err(nullError);
return Result.err(errForNull);
}
if (value === undefined) {
return Result.err(undefinedError);
return Result.err(errForUndefined);
}
return Result.ok(value);
@ -517,19 +562,17 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
* ```
*/
unwrap(): Promise<Res<T, E>>;
unwrap(fallback: NonNullable<T>): Promise<NonNullable<T>>;
unwrap(
fallback?: NonNullable<T>
): Promise<Res<T, E>> | Promise<NonNullable<T>> {
unwrap(fallback: T): Promise<T>;
unwrap(fallback?: T): Promise<Res<T, E>> | Promise<T> {
return fallback === undefined
? this.asyncResult.then<Res<T, E>>((res) => res.unwrap())
: this.asyncResult.then<NonNullable<T>>((res) => res.unwrap(fallback));
: this.asyncResult.then<T>((res) => res.unwrap(fallback));
}
/**
* Returns the ok-value or throw the error.
*/
async unwrapOrThrow(): Promise<NonNullable<T>> {
async unwrapOrThrow(): Promise<T> {
const result = await this.asyncResult;
return result.unwrapOrThrow();
}
@ -553,30 +596,28 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
*
* ```
*/
transform<U, EE>(
fn: (value: NonNullable<T>) => Result<U, E | EE>
transform<U extends Val, EE extends Val>(
fn: (value: T) => Result<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U, EE>(
fn: (value: NonNullable<T>) => AsyncResult<U, E | EE>
transform<U extends Val, EE extends Val>(
fn: (value: T) => AsyncResult<U, E | EE>
): AsyncResult<U, E | EE>;
transform<U, Input = any>(
fn: (value: NonNullable<T>) => SafeParseReturnType<Input, NonNullable<U>>
transform<U extends Val, Input = any>(
fn: (value: T) => SafeParseReturnType<Input, NonNullable<U>>
): AsyncResult<U, E | ZodError<Input>>;
transform<U, Input = any>(
fn: (
value: NonNullable<T>
) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
transform<U extends Val, Input = any>(
fn: (value: T) => Promise<SafeParseReturnType<Input, NonNullable<U>>>
): AsyncResult<U, E | ZodError<Input>>;
transform<U, EE>(
fn: (value: NonNullable<T>) => Promise<Result<U, E | EE>>
transform<U extends Val, EE extends Val>(
fn: (value: T) => Promise<Result<U, E | EE>>
): AsyncResult<U, E | EE>;
transform<U>(
fn: (value: NonNullable<T>) => Promise<RawValue<U>>
transform<U extends Val>(
fn: (value: T) => Promise<RawValue<U>>
): AsyncResult<U, E>;
transform<U>(fn: (value: NonNullable<T>) => RawValue<U>): AsyncResult<U, E>;
transform<U, EE, Input = any>(
transform<U extends Val>(fn: (value: T) => RawValue<U>): AsyncResult<U, E>;
transform<U extends Val, EE extends Val, Input = any>(
fn: (
value: NonNullable<T>
value: T
) =>
| Result<U, E | EE>
| AsyncResult<U, E | EE>
@ -632,16 +673,16 @@ export class AsyncResult<T, E> implements PromiseLike<Result<T, E>> {
);
}
catch<U = T, EE = E>(
catch<U extends Val = T, EE extends Val = E>(
fn: (err: NonNullable<E>) => Result<U, E | EE>
): AsyncResult<T | U, E | EE>;
catch<U = T, EE = E>(
catch<U extends Val = T, EE extends Val = E>(
fn: (err: NonNullable<E>) => AsyncResult<U, E | EE>
): AsyncResult<T | U, E | EE>;
catch<U = T, EE = E>(
catch<U extends Val = T, EE extends Val = E>(
fn: (err: NonNullable<E>) => Promise<Result<U, E | EE>>
): AsyncResult<T | U, E | EE>;
catch<U = T, EE = E>(
catch<U extends Val = T, EE extends Val = E>(
fn: (
err: NonNullable<E>
) => Result<U, E | EE> | AsyncResult<U, E | EE> | Promise<Result<U, E | EE>>