diff --git a/jsr.json b/jsr.json index cf01596..72311b2 100644 --- a/jsr.json +++ b/jsr.json @@ -1,5 +1,5 @@ { "name": "@inaiat/resultar", - "version": "0.8.6", + "version": "0.8.7", "exports": "./src/index.ts" } \ No newline at end of file diff --git a/package.json b/package.json index fee14c3..c1d57c9 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "resultar", - "version": "0.8.6", + "version": "0.8.7", "description": "Result pattern for typescript", "type": "module", "packageManager": "pnpm@8.15.6", @@ -50,7 +50,7 @@ }, "homepage": "https://github.com/inaiat/resultar", "devDependencies": { - "@types/node": "^20.12.5", + "@types/node": "^20.12.6", "eslint-plugin-unused-imports": "^3.1.0", "husky": "^9.0.11", "rimraf": "^5.0.5", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index d5ece25..b7106f2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -6,8 +6,8 @@ settings: devDependencies: '@types/node': - specifier: ^20.12.5 - version: 20.12.5 + specifier: ^20.12.6 + version: 20.12.6 eslint-plugin-unused-imports: specifier: ^3.1.0 version: 3.1.0(@typescript-eslint/eslint-plugin@7.6.0)(eslint@8.57.0) @@ -589,8 +589,8 @@ packages: resolution: {integrity: sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==} dev: true - /@types/node@20.12.5: - resolution: {integrity: sha512-BD+BjQ9LS/D8ST9p5uqBxghlN+S42iuNxjsUGjeZobe/ciXzk2qb1B6IXc6AnRLS+yFJRpN2IPEHMzwspfDJNw==} + /@types/node@20.12.6: + resolution: {integrity: sha512-3KurE8taB8GCvZBPngVbp0lk5CKi8M9f9k1rsADh0Evdz5SzJ+Q+Hx9uHoFGsLnLnd1xmkDQr2hVhlA0Mn0lKQ==} dependencies: undici-types: 5.26.5 dev: true @@ -2694,7 +2694,7 @@ packages: resolution: {integrity: sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==} engines: {node: '>= 10.13.0'} dependencies: - '@types/node': 20.12.5 + '@types/node': 20.12.6 merge-stream: 2.0.0 supports-color: 8.1.1 dev: true diff --git a/src/result-async.ts b/src/result-async.ts index 014d9f0..7db17f9 100644 --- a/src/result-async.ts +++ b/src/result-async.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/ban-types */ + import { Result, } from './result.js' @@ -16,6 +17,91 @@ import { combineResultAsyncListWithAllErrors, } from './utils.js' +// Combines the array of async results into one result. +export type CombineResultAsyncs< + T extends ReadonlyArray>, +> = IsLiteralArray extends 1 + ? TraverseAsync> + : ResultAsync, ExtractErrAsyncTypes[number]> + +// Combines the array of async results into one result with all errors. +export type CombineResultsWithAllErrorsArrayAsync< + T extends ReadonlyArray>, +> = IsLiteralArray extends 1 + ? TraverseWithAllErrorsAsync> + : ResultAsync, Array[number]>> + +// Unwraps the inner `Result` from a `ResultAsync` for all elements. +type UnwrapAsync = IsLiteralArray extends 1 + ? Writable extends [infer H, ...infer Rest] + ? H extends PromiseLike + ? HI extends Result + ? [Dedup, ...UnwrapAsync] + : never + : never + : [] + : // If we got something too general such as ResultAsync[] then we + // simply need to map it to ResultAsync. Yet `ResultAsync` + // itself is a union therefore it would be enough to cast it to Ok. + T extends Array + ? A extends PromiseLike + ? HI extends Result + ? Array> + : never + : never + : never + +// Traverse through the tuples of the async results and create one +// `ResultAsync` where the collected tuples are merged. +type TraverseAsync = IsLiteralArray extends 1 + ? Combine extends [infer Oks, infer Errs] + ? ResultAsync, MembersToUnion> + : never + : // The following check is important if we somehow reach to the point of + // checking something similar to ResultAsync[]. In this case we don't + // know the length of the elements, therefore we need to traverse the X and Y + // in a way that the result should contain X[] and Y[]. + T extends Array + ? // The MemberListOf here is to include all possible types. Therefore + // if we face (ResultAsync | ResultAsync)[] this type should + // handle the case. + Combine, Depth> extends [infer Oks, infer Errs] + ? // The following `extends unknown[]` checks are just to satisfy the TS. + // we already expect them to be an array. + Oks extends unknown[] + ? Errs extends unknown[] + ? ResultAsync>, MembersToUnion>> + : ResultAsync>, Errs> + : // The rest of the conditions are to satisfy the TS and support + // the edge cases which are not really expected to happen. + Errs extends unknown[] + ? ResultAsync>> + : ResultAsync + : never + : never + +// This type is similar to the `TraverseAsync` while the errors are also +// collected in order. For the checks/conditions made here, see that type +// for the documentation. +type TraverseWithAllErrorsAsync = IsLiteralArray extends 1 + ? Combine extends [infer Oks, infer Errs] + ? ResultAsync, EmptyArrayToNever> + : never + : Writable extends Array + ? Combine, Depth> extends [infer Oks, infer Errs] + ? Oks extends unknown[] + ? Errs extends unknown[] + ? ResultAsync>, EmptyArrayToNever>> + : ResultAsync>, Errs> + : Errs extends unknown[] + ? ResultAsync>> + : ResultAsync + : never + : never + +// Converts a reaodnly array into a writable array +type Writable = T extends readonly unknown[] ? [...T] : T + export class ResultAsync implements PromiseLike> { static fromSafePromise(promise: PromiseLike): ResultAsync static fromSafePromise(promise: Promise): ResultAsync { @@ -118,10 +204,29 @@ export class ResultAsync implements PromiseLike> { ) } + orElse>(f: (e: E) => R): ResultAsync> + orElse>(f: (e: E) => R): ResultAsync> + orElse(f: (e: E) => Result | ResultAsync): ResultAsync + orElse(f: any): any { + return new ResultAsync( + this.innerPromise.then(async (res: Result) => { // eslint-disable-line @typescript-eslint/no-unsafe-argument + if (res.isErr()) { + return f(res.error) // eslint-disable-line @typescript-eslint/no-unsafe-return + } + + return okAsync(res.value) + }), + ) + } + async match(ok: (t: T) => X, _err: (e: E) => X): Promise { return this.innerPromise.then(res => res.match(ok, _err)) } + async unwrapOr(t: A): Promise { + return this.innerPromise.then(res => res.unwrapOr(t)) + } + tap(f: (t: T) => void | Promise): ResultAsync { return new ResultAsync( this.innerPromise.then(async res => { @@ -202,87 +307,3 @@ export function safeTryAsync( ) } -// Combines the array of async results into one result. -export type CombineResultAsyncs< - T extends ReadonlyArray>, -> = IsLiteralArray extends 1 - ? TraverseAsync> - : ResultAsync, ExtractErrAsyncTypes[number]> - -// Combines the array of async results into one result with all errors. -export type CombineResultsWithAllErrorsArrayAsync< - T extends ReadonlyArray>, -> = IsLiteralArray extends 1 - ? TraverseWithAllErrorsAsync> - : ResultAsync, Array[number]>> - -// Unwraps the inner `Result` from a `ResultAsync` for all elements. -type UnwrapAsync = IsLiteralArray extends 1 - ? Writable extends [infer H, ...infer Rest] - ? H extends PromiseLike - ? HI extends Result - ? [Dedup, ...UnwrapAsync] - : never - : never - : [] - : // If we got something too general such as ResultAsync[] then we - // simply need to map it to ResultAsync. Yet `ResultAsync` - // itself is a union therefore it would be enough to cast it to Ok. - T extends Array - ? A extends PromiseLike - ? HI extends Result - ? Array> - : never - : never - : never - -// Traverse through the tuples of the async results and create one -// `ResultAsync` where the collected tuples are merged. -type TraverseAsync = IsLiteralArray extends 1 - ? Combine extends [infer Oks, infer Errs] - ? ResultAsync, MembersToUnion> - : never - : // The following check is important if we somehow reach to the point of - // checking something similar to ResultAsync[]. In this case we don't - // know the length of the elements, therefore we need to traverse the X and Y - // in a way that the result should contain X[] and Y[]. - T extends Array - ? // The MemberListOf here is to include all possible types. Therefore - // if we face (ResultAsync | ResultAsync)[] this type should - // handle the case. - Combine, Depth> extends [infer Oks, infer Errs] - ? // The following `extends unknown[]` checks are just to satisfy the TS. - // we already expect them to be an array. - Oks extends unknown[] - ? Errs extends unknown[] - ? ResultAsync>, MembersToUnion>> - : ResultAsync>, Errs> - : // The rest of the conditions are to satisfy the TS and support - // the edge cases which are not really expected to happen. - Errs extends unknown[] - ? ResultAsync>> - : ResultAsync - : never - : never - -// This type is similar to the `TraverseAsync` while the errors are also -// collected in order. For the checks/conditions made here, see that type -// for the documentation. -type TraverseWithAllErrorsAsync = IsLiteralArray extends 1 - ? Combine extends [infer Oks, infer Errs] - ? ResultAsync, EmptyArrayToNever> - : never - : Writable extends Array - ? Combine, Depth> extends [infer Oks, infer Errs] - ? Oks extends unknown[] - ? Errs extends unknown[] - ? ResultAsync>, EmptyArrayToNever>> - : ResultAsync>, Errs> - : Errs extends unknown[] - ? ResultAsync>> - : ResultAsync - : never - : never - -// Converts a reaodnly array into a writable array -type Writable = T extends readonly unknown[] ? [...T] : T diff --git a/tests/result-test.ts b/tests/result-test.ts index c407302..1471ad7 100644 --- a/tests/result-test.ts +++ b/tests/result-test.ts @@ -486,38 +486,6 @@ await describe('ResultAsync', async () => { }) }) - await it('Maps a value using an asynchronous function', async () => { - const asyncVal = okAsync(12) - - const mapAsyncFn = mock.fn(async (v: number) => v.toString()) - - const mapped = asyncVal.map(mapAsyncFn) - - isTrue(mapped instanceof ResultAsync) - - const newVal = await mapped - - isTrue(newVal.isOk()) - equal(newVal._unsafeUnwrap(), '12') - equal(mapAsyncFn.mock.calls.length, 1) - }) - - await it('Skips an error', async () => { - const asyncErr = errAsync('Wrong format') - - const mapSyncFn = mock.fn((v: number) => v.toString()) - - const notMapped = asyncErr.map(mapSyncFn) - - isTrue(notMapped instanceof ResultAsync) - - const newVal = await notMapped - - isTrue(newVal.isErr()) - equal(newVal._unsafeUnwrapErr(), 'Wrong format') - equal(mapSyncFn.mock.calls.length, 0) - }) - await describe('mapErr', async () => { await it('Maps an error using a synchronous function', async () => { const asyncErr = errAsync('Wrong format') @@ -580,6 +548,207 @@ await describe('ResultAsync', async () => { }) }) + await describe('andThen', async () => { + await it('Maps a value using a function returning a ResultAsync', async () => { + const asyncVal = okAsync(12) + + const andThenResultAsyncFn = mock.fn(() => okAsync('good')) + + const mapped = asyncVal.andThen(andThenResultAsyncFn) + + isTrue(mapped instanceof ResultAsync) + + const newVal = await mapped + + isTrue(newVal.isOk()) + equal(newVal._unsafeUnwrap(), 'good') + equal(andThenResultAsyncFn.mock.calls.length, 1) + }) + + await it('Maps a value using a function returning a Result', async () => { + const asyncVal = okAsync(12) + + const andThenResultFn = mock.fn(() => ok('good')) + + const mapped = asyncVal.andThen(andThenResultFn) + + isTrue(mapped instanceof ResultAsync) + + const newVal = await mapped + + isTrue(newVal.isOk()) + equal(newVal._unsafeUnwrap(), 'good') + equal(andThenResultFn.mock.calls.length, 1) + }) + + await it('Skips an Error', async () => { + const asyncVal = errAsync('Wrong format') + + const andThenResultFn = mock.fn(() => ok('good')) + + const notMapped = asyncVal.andThen(andThenResultFn) + + isTrue(notMapped instanceof ResultAsync) + + const newVal = await notMapped + + isTrue(newVal.isErr()) + equal(newVal._unsafeUnwrapErr(), 'Wrong format') + equal(andThenResultFn.mock.calls.length, 0) + }) + }) + + await describe('orElse', async () => { + await it('Skips orElse on an Ok value', async () => { + const okVal = okAsync(12) + const errorCallback = mock.fn(_errVal => errAsync('It is now a string')) + + const result = await okVal.orElse(errorCallback) + + deepEqual(result, ok(12)) + + equal(errorCallback.mock.calls.length, 0) + }) + + await it('Invokes the orElse callback on an Err value', async () => { + const myResult = errAsync('BOOOM!') + const errorCallback = mock.fn(_errVal => errAsync(true)) + + const result = await myResult.orElse(errorCallback) + + deepEqual(result, err(true)) + equal(errorCallback.mock.calls.length, 1) + }) + + await it('Accepts a regular Result in the callback', async () => { + const myResult = errAsync('BOOOM!') + const errorCallback = mock.fn(_errVal => err(true)) + + const result = await myResult.orElse(errorCallback) + + deepEqual(result, err(true)) + equal(errorCallback.mock.calls.length, 1) + }) + }) + + await describe('match', async () => { + await it('Matches on an Ok', async () => { + const okMapper = mock.fn(_val => 'weeeeee') + const errMapper = mock.fn(_val => 'wooooo') + + const matched = await okAsync(12).match(okMapper, errMapper) + + equal(matched, 'weeeeee') + equal(okMapper.mock.calls.length, 1) + equal(errMapper.mock.calls.length, 0) + }) + + await it('Matches on an Error', async () => { + const okMapper = mock.fn(_val => 'weeeeee') + const errMapper = mock.fn(_val => 'wooooo') + + const matched = await errAsync('bad').match(okMapper, errMapper) + + equal(matched, 'wooooo') + equal(okMapper.mock.calls.length, 0) + equal(errMapper.mock.calls.length, 1) + }) + }) + + await describe('unwrapOr', async () => { + await it('returns a promise to the result value on an Ok', async () => { + const unwrapped = await okAsync(12).unwrapOr(10) + equal(unwrapped, 12) + }) + + await it('returns a promise to the provided default value on an Error', async () => { + const unwrapped = await errAsync(12).unwrapOr(10) + equal(unwrapped, 10) + }) + }) + + await describe('fromSafePromise', async () => { + await it('Creates a ResultAsync from a Promise', async () => { + const res = ResultAsync.fromSafePromise(Promise.resolve(12)) + + equal(res instanceof ResultAsync, true) + + const val = await res + isTrue(val.isOk()) + equal(val._unsafeUnwrap(), 12) + }) + }) + + await describe('fromPromise', async () => { + await it('Accepts an error handler as a second argument', async () => { + const res = ResultAsync.fromPromise(Promise.reject('No!'), e => new Error('Oops: '.concat(String(e)))) + + equal(res instanceof ResultAsync, true) + + const val = await res + isTrue(val.isErr()) + deepEqual(val._unsafeUnwrapErr(), new Error('Oops: No!')) + }) + }) + + await describe('okAsync', async () => { + await it('Creates a ResultAsync that resolves to an Ok', async () => { + const val = okAsync(12) + + equal(val instanceof ResultAsync, true) + + const res = await val + + isTrue(res.isOk()) + equal(res._unsafeUnwrap(), 12) + }) + }) + + await describe('errAsync', async () => { + await it('Creates a ResultAsync that resolves to an Err', async () => { + const err = errAsync('bad') + + equal(err instanceof ResultAsync, true) + + const res = await err + + isTrue(res.isErr()) + equal(res._unsafeUnwrapErr(), 'bad') + }) + }) + + await it('Maps a value using an asynchronous function', async () => { + const asyncVal = okAsync(12) + + const mapAsyncFn = mock.fn(async (v: number) => v.toString()) + + const mapped = asyncVal.map(mapAsyncFn) + + isTrue(mapped instanceof ResultAsync) + + const newVal = await mapped + + isTrue(newVal.isOk()) + equal(newVal._unsafeUnwrap(), '12') + equal(mapAsyncFn.mock.calls.length, 1) + }) + + await it('Skips an error', async () => { + const asyncErr = errAsync('Wrong format') + + const mapSyncFn = mock.fn((v: number) => v.toString()) + + const notMapped = asyncErr.map(mapSyncFn) + + isTrue(notMapped instanceof ResultAsync) + + const newVal = await notMapped + + isTrue(newVal.isErr()) + equal(newVal._unsafeUnwrapErr(), 'Wrong format') + equal(mapSyncFn.mock.calls.length, 0) + }) + await describe('Result.fromThrowable', async () => { await it('Parser JSON', async () => { const safeJSONParse = (text: string, reviver?: (this: unknown, key: string, value: unknown) => unknown) => Result.fromThrowable(JSON.parse, () => 'parser error')(text, reviver) as Result<{name: string}, string>