diff --git a/src/Err.ts b/src/Err.ts index 7bc1c6f..7505f10 100644 --- a/src/Err.ts +++ b/src/Err.ts @@ -91,6 +91,10 @@ export class ErrImpl implements Err { biChain(_: unknown, errFn: (error: E) => Result): Result { return errFn(this.error); } + + apply() { + return this; + } } Object.defineProperty(ErrImpl, 'name', { enumerable: false, value: 'Err' }); diff --git a/src/Ok.ts b/src/Ok.ts index 1a6e849..bf2931b 100644 --- a/src/Ok.ts +++ b/src/Ok.ts @@ -1,4 +1,5 @@ -import type { AsyncOk, Ok, Result } from './types'; +import { resolveOks } from './resolve-args'; +import type { AsyncOk, ErrTypeOf, Ok, ResolveOks, Result } from './types'; export class OkImpl implements Ok { constructor(public readonly value: T) {} @@ -87,6 +88,19 @@ export class OkImpl implements Ok { biChain(okFn: (data: T) => Result): Result { return okFn(this.value); } + + apply( + this: OkImpl<(...args: ResolveOks) => R>, + ...args: Args + ): Result> { + if (typeof this.value !== 'function') { + throw new TypeError('Result.value is not a function', { cause: this }); + } + + const argValues = resolveOks(args); + + return Array.isArray(argValues) ? ok(this.value(...argValues)) : argValues; + } } Object.defineProperty(OkImpl, 'name', { enumerable: false, value: 'Ok' }); diff --git a/src/resolve-args.ts b/src/resolve-args.ts new file mode 100644 index 0000000..f9736c3 --- /dev/null +++ b/src/resolve-args.ts @@ -0,0 +1,20 @@ +import { isResult } from './guards'; +import { Err, ErrTypeOf, ResolveOks } from './types'; + +export const resolveOks = ( + args: PR, +): ResolveOks | Err> => { + const argValues = [] as any[]; + + for (const arg of args) { + if (!isResult(arg)) { + argValues.push(arg); + } else if (arg.isErr) { + return arg as Err>; + } else { + argValues.push(arg.value); + } + } + + return argValues as ResolveOks; +}; diff --git a/src/result.test.ts b/src/result.test.ts index 5e56963..e0021c3 100644 --- a/src/result.test.ts +++ b/src/result.test.ts @@ -1,14 +1,14 @@ -import { describe, it, expect, jest } from '@jest/globals'; +import { describe, expect, it, jest } from '@jest/globals'; import { Equal, Expect } from '@type-challenges/utils'; -import { pipe } from './fn/pipe'; -import { identity } from './fn/identity'; -import { compose2 } from './fn/compose'; -import { Err, Ok, Result } from './types'; -import * as R from './sync-methods'; import * as okIf from './conditional'; +import { err } from './Err'; +import { compose2 } from './fn/compose'; +import { identity } from './fn/identity'; +import { pipe } from './fn/pipe'; import * as Guards from './guards'; import { ok } from './Ok'; -import { err } from './Err'; +import * as R from './sync-methods'; +import { Result } from './types'; describe('Result', () => { describe('isResult', () => { @@ -57,7 +57,7 @@ describe('Result', () => { }); it('should narrow type in if statement', () => { - expect.assertions(3); // ensure that both if and else branches are executed + expect.assertions(2); // ensure that both if and else branches are executed const result = ok('foo') as Result<'foo', string>; type FirstCheck = Expect>>; @@ -65,17 +65,14 @@ describe('Result', () => { expect(check1).toBe(true); if (Guards.isOk(result)) { - type Check = Expect>>; type CheckValue = Expect>; - const check: Check = true; const checkValue: CheckValue = true; - expect(check).toBe(true); expect(checkValue).toBe(true); } }); it('should narrow type in else branch of if statement', () => { - expect.assertions(3); // ensure that both if and else branches are executed + expect.assertions(2); // ensure that both if and else branches are executed const result = err('bar') as Result<'foo', string>; type FirstCheck = Expect>>; @@ -85,11 +82,8 @@ describe('Result', () => { if (Guards.isOk(result)) { // eslint-disable-next-line no-trailing-spaces } else { - type Check = Expect>>; type CheckValue = Expect>; - const check: Check = true; const checkValue: CheckValue = true; - expect(check).toBe(true); expect(checkValue).toBe(true); } }); @@ -103,7 +97,7 @@ describe('Result', () => { expect(check1).toBe(true); if (result.isOk) { - type Check = Expect>>; + type Check = Expect>; const check: Check = true; expect(check).toBe(true); } @@ -120,7 +114,7 @@ describe('Result', () => { if (result.isOk) { // eslint-disable-next-line no-trailing-spaces } else { - type Check = Expect>>; + type Check = Expect>; const check: Check = true; expect(check).toBe(true); } @@ -146,7 +140,7 @@ describe('Result', () => { expect(check1).toBe(true); if (Guards.isErr(result)) { - type Check = Expect>>; + type Check = Expect>; const check: Check = true; expect(check).toBe(true); } @@ -164,7 +158,7 @@ describe('Result', () => { if (Guards.isErr(result)) { // eslint-disable-next-line no-trailing-spaces } else { - type Check = Expect>>; + type Check = Expect>; const check: Check = true; expect(check).toBe(true); } @@ -180,7 +174,7 @@ describe('Result', () => { expect(check1).toBe(true); if (result.isErr) { - type Check = Expect>>; + type Check = Expect>; const check: Check = true; expect(check).toBe(true); } @@ -198,7 +192,7 @@ describe('Result', () => { if (result.isErr) { // eslint-disable-next-line no-trailing-spaces } else { - type Check = Expect>>; + type Check = Expect>; const check: Check = true; expect(check).toBe(true); } @@ -846,16 +840,33 @@ describe('Result', () => { expect(pipe(result, R.apply(ok(1)))).toEqual(ok(1)); }); + it('returns the Ok if applied on Ok(idX) - method', () => { + const result = ok(identity).apply(ok(1)); + expect(result).toEqual(ok(1)); + }); + it('returns the Err if applied on Err(idX)', () => { const result = err(identity); expect(pipe(result, R.apply(ok(1)))).toEqual(result); }); + it('returns the Err if applied on Err("foo") - method', () => { + const result = err('foo').apply(ok(1)); + expect(result).toBe(result); + }); + it('returns the Err if applied on Ok(idX) with Err', () => { const result = ok(identity); expect(pipe(result, R.apply(err(1)))).toEqual(err(1)); }); + it('returns the Err if applied on Ok(idX) with Err - method', () => { + const result = ok(identity).apply( + err('ERR') as Result, + ); + expect(result).toEqual(err('ERR')); + }); + it('returns Ok if applied on Ok(x => y => [x, y]) with Oks', () => { const result = ok((x: number) => (y: string) => [x, y]); expect(pipe(result, R.apply(ok(1)), R.apply(ok('foo')))).toEqual( @@ -863,6 +874,13 @@ describe('Result', () => { ); }); + it('returns Ok if applied on Ok(x => y => [x, y]) with Oks - method', () => { + const result = ok((x: number) => (y: string) => [x, y]) + .apply(ok(1)) + .apply(ok('foo')); + expect(result).toEqual(ok([1, 'foo'])); + }); + it('returns a correctly typed Result if applied on Ok(x => y => [x, y]) with Oks', () => { const result = pipe( ok((x: number) => (y: string) => [x, y] as const), @@ -877,6 +895,18 @@ describe('Result', () => { expect(check).toBe(true); }); + it('returns a correctly typed Result if applied on Ok(x => y => [x, y]) with Oks - method', () => { + const result = ok((x: number) => (y: string) => [x, y] as const) + .apply(ok(1)) + .apply(ok('foo')); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + it('returns a correctly typed Result if applied on Ok(x => y => [x, y]) with Oks typed as Results', () => { const result = pipe( ok((x: number) => (y: string) => [x, y] as const) as Result< @@ -897,11 +927,38 @@ describe('Result', () => { expect(check).toBe(true); }); - it('returns the Ok if applied on Ok(x => y => [x, y]) with pure params', () => { + it('returns a correctly typed Result if applied on Ok(x => y => [x, y]) with Oks typed as Results - method', () => { + const fnRes = ok((x: number) => (y: string) => [x, y] as const) as Result< + (x: number) => (y: string) => readonly [number, string], + 'ERR' + >; + const arg1Res = ok(1) as Result; + const arg2Res = ok('foo') as Result; + + const result = fnRes.apply(arg1Res).apply(arg2Res); + + const check: Expect< + Equal< + typeof result, + Result + > + > = true; + + expect(check).toBe(true); + }); + + it('returns Ok if applied on Ok(x => y => [x, y]) with pure params', () => { const result = ok((x: number) => (y: string) => [x, y]); expect(pipe(result, R.apply(1), R.apply('foo'))).toEqual(ok([1, 'foo'])); }); + it('returns Ok if applied on Ok(x => y => [x, y]) with pure params - method', () => { + const result = ok((x: number) => (y: string) => [x, y]) + .apply(1) + .apply('foo'); + expect(result).toEqual(ok([1, 'foo'])); + }); + it('returns the correctly typed Result if applied on Ok(x => y => [x, y]) with pure params', () => { const result = pipe( ok((x: number) => (y: string) => [x, y]), @@ -916,10 +973,246 @@ describe('Result', () => { expect(check).toBe(true); }); + it('returns the correctly typed Result if applied on Ok(x => y => [x, y]) with pure params - method', () => { + const result = ok((x: number) => (y: string) => [x, y]) + .apply(1) + .apply('foo'); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns the correctly typed Result if applied on Result(x => y => [x, y], "ERR") with pure params', () => { + type R = Result<(x: number) => (y: string) => (number | string)[], 'ERR'>; + const result = pipe( + ok((x: number) => (y: string) => [x, y]) as R, + R.apply(1), + R.apply('foo'), + ); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns the correctly typed Result if applied on Result(x => y => [x, y], "ERR") with pure params - method', () => { + type R = Result<(x: number) => (y: string) => (number | string)[], 'ERR'>; + const result = (ok((x: number) => (y: string) => [x, y]) as R) + .apply(1) + .apply('foo'); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + it('throws an error if applied on Ok(not a function) with Oks', () => { expect(() => pipe(ok(1 as any), R.apply(ok(1)))).toThrowError( new TypeError('Result.value is not a function', { cause: ok(1) }), ); }); + + it('throws an error if applied on Ok(not a function) with Oks - method', () => { + expect(() => ok(1 as any).apply(ok(1))).toThrowError( + new TypeError('Result.value is not a function', { cause: ok(1) }), + ); + }); + + it('returns Ok if applied on Ok((x, y) => [x, y]) with Oks', () => { + const result = ok((x: number, y: string) => [x, y]); + expect(pipe(result, R.apply(ok(1), ok('foo')))).toEqual(ok([1, 'foo'])); + }); + + it('returns Ok if applied on Ok((x, y) => [x, y]) with Oks - method', () => { + const result = ok((x: number, y: string) => [x, y]).apply( + ok(1), + ok('foo'), + ); + expect(result).toEqual(ok([1, 'foo'])); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with Oks', () => { + const result = pipe( + ok((x: number, y: string) => [x, y] as const), + R.apply(ok(1), ok('foo')), + ); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with Oks - method', () => { + const result = ok((x: number, y: string) => [x, y] as const).apply( + ok(1), + ok('foo'), + ); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with Oks typed as Results', () => { + const result = pipe( + ok((x: number, y: string) => [x, y] as const) as Result< + (x: number, y: string) => readonly [number, string], + 'ERR' + >, + R.apply( + ok(1) as Result, + ok('foo') as Result, + ), + ); + + const check: Expect< + Equal< + typeof result, + Result + > + > = true; + + expect(check).toBe(true); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with Oks typed as Results - method', () => { + const result = ( + ok((x: number, y: string) => [x, y] as const) as Result< + (x: number, y: string) => readonly [number, string], + 'ERR' + > + ).apply( + ok(1) as Result, + ok('foo') as Result, + ); + + const check: Expect< + Equal< + typeof result, + Result + > + > = true; + + expect(check).toBe(true); + }); + + it('returns Ok if applied on Ok((x, y) => [x, y]) with pure params', () => { + const result = ok((x: number, y: string) => [x, y]); + expect(pipe(result, R.apply(1, 'foo'))).toEqual(ok([1, 'foo'])); + }); + + it('returns Ok if applied on Ok((x, y) => [x, y]) with pure params - method', () => { + const result = ok((x: number, y: string) => [x, y]).apply(1, 'foo'); + + expect(result).toEqual(ok([1, 'foo'])); + }); + + it('returns the correctly typed Result if applied on Ok((x, y) => [x, y]) with pure params', () => { + const result = pipe( + ok((x: number, y: string) => [x, y]), + R.apply(1, 'foo'), + ); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns the correctly typed Result if applied on Ok((x, y) => [x, y]) with pure params - method', () => { + const result = ok((x: number, y: string) => [x, y]).apply(1, 'foo'); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns the correctly typed Result if applied on Result((x, y) => [x, y], "ERR") with pure params', () => { + type R = Result<(x: number, y: string) => (number | string)[], 'ERR'>; + const result = pipe( + ok((x: number, y: string) => [x, y]) as R, + R.apply(1, 'foo'), + ); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns the correctly typed Result if applied on Result((x, y) => [x, y], "ERR") with pure params - method', () => { + type R = Result<(x: number, y: string) => (number | string)[], 'ERR'>; + const result = (ok((x: number, y: string) => [x, y]) as R).apply( + 1, + 'foo', + ); + + const check: Expect< + Equal> + > = true; + + expect(check).toBe(true); + }); + + it('returns Ok if applied on Ok((x, y) => [x, y]) with all arguments', () => { + const result = ok((x: number, y: string) => [x, y]); + expect(result.apply(ok(1), ok('foo'))).toEqual(ok([1, 'foo'])); + }); + + it('returns Ok if applied on Ok((x, y) => [x, y]) with all arguments - method', () => { + const result = ok((x: number, y: string) => [x, y]); + expect(result.apply(ok(1), ok('foo'))).toEqual(ok([1, 'foo'])); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with all arguments', () => { + const result = ok((x: number, y: string) => [x, y] as const); + expect(result.apply(ok(1), ok('foo'))).toEqual(ok([1, 'foo'])); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with all arguments - method', () => { + const result = ok((x: number, y: string) => [x, y] as const); + expect(result.apply(ok(1), ok('foo'))).toEqual(ok([1, 'foo'])); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with all arguments typed as Results', () => { + const result = ok((x: number, y: string) => [x, y] as const) as Result< + (x: number, y: string) => readonly [number, string], + 'ERR' + >; + expect( + result.apply( + ok(1) as Result, + ok('foo') as Result, + ), + ).toEqual(ok([1, 'foo'])); + }); + + it('returns a correctly typed Result if applied on Ok((x, y) => [x, y]) with all arguments typed as Results - method', () => { + const result = ok((x: number, y: string) => [x, y] as const) as Result< + (x: number, y: string) => readonly [number, string], + 'ERR' + >; + expect( + result.apply( + ok(1) as Result, + ok('foo') as Result, + ), + ).toEqual(ok([1, 'foo'])); + }); }); }); diff --git a/src/sync-methods.ts b/src/sync-methods.ts index 3a3c10a..7f1ec00 100644 --- a/src/sync-methods.ts +++ b/src/sync-methods.ts @@ -1,6 +1,4 @@ -import { isResult } from './guards.js'; -import { ok } from './Ok.js'; -import type { Err, ErrTypeOf, Result } from './types'; +import type { ErrTypeOf, ResolveOks, Result } from './types'; export const map = (fn: (data: T) => S) => @@ -66,35 +64,10 @@ export const tapErr = (result: Result): Result => result.tapErr(fn); -type ResolveOks

= { - [K in keyof P]: P[K] extends Result ? T : P[K]; -}; - export const apply = (...args: PR) => - ( - result: Result<(...args: ResolveOks) => T, E>, - ): Result> => { - if (result.isErr) return result; - - if (typeof result.value !== 'function') { - throw new TypeError('Result.value is not a function', { cause: result }); - } - - const argValues = [] as any[]; - - for (const arg of args) { - if (!isResult(arg)) { - argValues.push(arg); - } else if (arg.isErr) { - return arg as Err>; - } else { - argValues.push(arg.value); - } - } - - return ok(result.value(...(argValues as any))); - }; + (result: Result<(...args: ResolveOks) => T, E>) => + result.apply(...args) as Result>; export const biMap = (okFn: (data: T) => S, errFn: (error: E) => F) => diff --git a/src/types.ts b/src/types.ts index 073b7a8..305e533 100644 --- a/src/types.ts +++ b/src/types.ts @@ -36,9 +36,26 @@ export interface ResultInterface { errFn: (error: E) => Result, ): Result; [Symbol.iterator](): Generator; + apply( + this: ResultInterface<(...args: ResolveOks) => R, E>, + ...args: Args + ): Result>; } -export type Result = Ok | Err; +export type MaybeResultsOf = { + [K in keyof T]: T[K] | Result; +}; + +export type ResolveOks

= { + [K in keyof P]: P[K] extends Result ? T : P[K]; +}; + +export type Result = (Ok | Err) & { + apply( + this: ResultInterface<(...args: ResolveOks) => R, E>, + ...args: Args + ): Result>; +}; export type NotResultOf = T extends Result ? never : T; export type ErrTypeOf = T extends Err ? E : never;