Skip to content

Commit

Permalink
Fix result and success/failure types compatability
Browse files Browse the repository at this point in the history
  • Loading branch information
alexshelkov committed Jul 16, 2021
1 parent 5f1df5a commit f44e748
Show file tree
Hide file tree
Showing 6 changed files with 133 additions and 74 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "lambda-res",
"description": "Type-safe error handling without exception",
"version": "1.0.6",
"version": "1.0.7",
"author": "Alex Shelkovskiy <alexshelkov@gmail.com>",
"repository": "https://github.com/alexshelkov/result",
"license": "MIT",
Expand Down
40 changes: 38 additions & 2 deletions src/__tests__/err.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable @typescript-eslint/ban-types */

import { Err } from '../index';
import { Err, isErr, isErrType } from '../index';

type TestErr<T, A = {}> = {
type: T;
Expand All @@ -11,7 +11,7 @@ type TestErr<T, A = {}> = {
fatal?: boolean;
} & A;

describe('error utils', () => {
describe('error type', () => {
it('string as type', () => {
expect.assertions(1);

Expand Down Expand Up @@ -58,3 +58,39 @@ describe('error utils', () => {
expect(e1.type).toStrictEqual('e1');
});
});

describe('error utils', () => {
it('is an error', () => {
expect.assertions(3);

expect(isErr({ type: 'test' })).toBeTruthy();

expect(isErr({})).toBeFalsy();

expect(isErr(null)).toBeFalsy();
});

it('is an error of type', () => {
expect.assertions(2);

const o1 = { type: 'test' as const };

// eslint-disable-next-line jest/no-if
if (isErrType('test', o1)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
o1 as { type: 'test' };
// eslint-disable-next-line jest/no-conditional-expect
expect(o1.type).toStrictEqual('test');
}

// eslint-disable-next-line jest/no-if
if (isErrType('test2', o1)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
o1 as never;
// eslint-disable-next-line jest/no-conditional-expect
expect(o1).toBeFalsy(); // this must be unreachable
}

expect(isErrType('test', {})).toBeFalsy();
});
});
133 changes: 76 additions & 57 deletions src/__tests__/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,4 @@
import {
Err,
Errs,
Success,
Failure,
Result,
ErrLevel,
ok,
nope,
fail,
err,
isErr,
isErrType,
} from '../index';
import { Err, Errs, Result, ErrLevel, ok, nope, fail, err, Failure, Success } from '../index';

type E1 = Err<'e1'>;

Expand All @@ -24,12 +11,77 @@ interface E3<T> extends Err {

type AppErr = E1 | E2 | E3<number>;

type Expect<T extends true> = T;
type Assert<A extends boolean, T extends A> = T;

type Equal<X, Y> = (<T>() => T extends X ? 1 : 2) extends <T>() => T extends Y ? 1 : 2
? true
: false;

describe('result and success/failure types compatability', () => {
it('success assignable to result', () => {
expect.assertions(2);

const r1: Success<string> = ok('ok1');

expect(r1.ok()).toStrictEqual('ok1');

const r2: Result<string, never> = ({ status: 'ok', data: 'ok2' } as unknown) as Success<string>;

expect(r2.data).toStrictEqual('ok2');
});

it('failure assignable to result', () => {
expect.assertions(2);

const r1: Failure<AppErr> = fail<E1>('e1');

expect(r1.err().type).toStrictEqual('e1');

const r2: Result<never, E1> = ({
status: 'error',
error: { type: 'e1' },
} as unknown) as Failure<E1>;

expect(r2.error.type).toStrictEqual('e1');
});

it('narrowing success type', () => {
expect.assertions(0);

const r1 = ok('ok1');

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR1 = Assert<false, Equal<Success<string>, typeof r1>>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR2 = Assert<true, Equal<Result<string, never>, typeof r1>>;

if (r1.isOk()) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR3 = Assert<true, Equal<Success<string>, typeof r1>>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR4 = Assert<false, Equal<Result<string, never>, typeof r1>>;
}
});

it('narrowing error type', () => {
expect.assertions(0);

const r1 = fail<E1>('e1');

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR1 = Assert<false, Equal<Failure<E1>, typeof r1>>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR2 = Assert<true, Equal<Result<never, E1>, typeof r1>>;

if (r1.isErr()) {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR3 = Assert<true, Equal<Failure<E1>, typeof r1>>;
// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpectedR4 = Assert<false, Equal<Result<never, E1>, typeof r1>>;
}
});
});

describe('result', () => {
it('sets the code correctly', () => {
expect.assertions(1);
Expand All @@ -50,7 +102,7 @@ describe('result', () => {
it('sets the message correctly', () => {
expect.assertions(2);

const result: Result<string, Err> = fail('', { message: 'Some error' });
const result: Result<string, Err<'e1'>> = fail('e1', { message: 'Some error' });

expect(result.message).toStrictEqual('Some error');
expect(result.err().message).toStrictEqual('Some error');
Expand Down Expand Up @@ -128,16 +180,19 @@ describe('result', () => {
it('will check errors exhaustively', () => {
expect.assertions(1);

const test1 = (e: Failure<AppErr>) => {
switch (e.error.type) {
const test1 = (r: Result<never, AppErr>) => {
const e = r.err();

// eslint-disable-next-line jest/no-if
switch (e.type) {
case 'e1':
return 1;
case 'e2':
return 2;
case 'e3':
return 3;
default:
nope(e.error);
nope(e);

return 0;
}
Expand Down Expand Up @@ -268,13 +323,13 @@ describe('result', () => {

expect(r1.ok()).toStrictEqual(1);

type R2 = Failure<Err<'e1' | 'e3'>> | Failure<E2> | Success<number>;
type R2 = Result<never, Err<'e1' | 'e3'>> | Result<never, E2> | Result<number, never>;

const e1 = fail<E2>('e2', { stringAdded: 'e2data' });
const r2 = Math.random() !== -1 ? e1 : r1;

// eslint-disable-next-line @typescript-eslint/no-unused-vars
type ExpextedR2 = Expect<Equal<R2, typeof r2>>;
type ExpextedR2 = Assert<true, Equal<R2, typeof r2>>;

expect(r2.err().type).toStrictEqual('e2');

Expand All @@ -287,39 +342,3 @@ describe('result', () => {
});
});
});

describe('error utils', () => {
it('is an error', () => {
expect.assertions(3);

expect(isErr({ type: 'test' })).toBeTruthy();

expect(isErr({})).toBeFalsy();

expect(isErr(null)).toBeFalsy();
});

it('is an error of type', () => {
expect.assertions(2);

const o1 = { type: 'test' as const };

// eslint-disable-next-line jest/no-if
if (isErrType('test', o1)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
o1 as { type: 'test' };
// eslint-disable-next-line jest/no-conditional-expect
expect(o1.type).toStrictEqual('test');
}

// eslint-disable-next-line jest/no-if
if (isErrType('test2', o1)) {
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
o1 as never;
// eslint-disable-next-line jest/no-conditional-expect
expect(o1).toBeFalsy(); // this must be unreachable
}

expect(isErrType('test', {})).toBeFalsy();
});
});
12 changes: 6 additions & 6 deletions src/__tests__/transform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -92,25 +92,25 @@ describe('chaining onOk and onErr', () => {
expect(rOk.ok()).toStrictEqual('r1');

const trOk = await rOk.onOk(async () => {
return ok(3) as Result<number, E2['e21']>;
return ok(3);
});

expect(trOk.ok()).toStrictEqual(3);

const trErr = await rOk.onOk(async () => {
return fail('e21') as Result<boolean, E2['e21']>;
return fail<E2['e21']>('e21');
});

expect(trErr.err().type).toStrictEqual('e21');

const tr1 = await trErr.onOk(async () => {
return ok({ tr1: true }) as Result<{ tr1: true }, E3['e31']>;
return ok({ tr1: true });
});

expect(tr1.err().type).toStrictEqual('e21');

const tr2 = await trErr.onOk(async () => {
return fail('e31') as Result<{ tr1: true }, E3['e31']>;
return fail<E3['e31']>('e31');
});

expect(tr2.err().type).toStrictEqual('e21');
Expand All @@ -132,7 +132,7 @@ describe('chaining onOk and onErr', () => {
const r1: Result<{ r1: true }, E1['e11']> = s1 ? ok({ r1: true }) : fail('e11');

const r2 = await r1.onOk(async () => {
return s2 ? ok({ r2: true }) : (fail('e21') as Result<{ r2: boolean }, E2['e21']>);
return (s2 ? ok({ r2: true }) : fail('e21')) as Result<{ r2: boolean }, E2['e21']>;
});

const r3 = r2.onErr(async () => {
Expand All @@ -158,7 +158,7 @@ describe('chaining onOk and onErr', () => {
});

const r3 = r2.onOk(async () => {
return s2 ? ok({ r2: true }) : (fail('e31') as Result<{ r2: boolean }, E3['e31']>);
return (s2 ? ok({ r2: true }) : fail('e31')) as Result<{ r2: boolean }, E3['e31']>;
});

return r3;
Expand Down
12 changes: 8 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ interface CompareResult {
export interface PartialSuccess<Data> extends CompareResult {
status: 'success';
data: Data;
message: never;
stack: never;
}

export interface PartialFailure<Fail> extends Error, CompareResult {
Expand Down Expand Up @@ -34,7 +36,7 @@ export interface Failure<Fail> extends PartialFailure<Fail> {

export interface Transform<Data, Fail> {
onOk<Data2, Fail2>(cb: (data: Data) => Response<Data2, Fail2>): Response<Data2, Fail | Fail2>;
onErr<Fail2>(cb: (err: Fail) => Promise<Failure<Fail2>>): Response<Data, Fail2>;
onErr<Fail2>(cb: (err: Fail) => Promise<Result<never, Fail2>>): Response<Data, Fail2>;
}

export const ErrLevel = {
Expand Down Expand Up @@ -83,8 +85,10 @@ export type Errs<Errors> = Errors[keyof Errors] extends Err

export type PartialResult<Data, Fail> = PartialSuccess<Data> | PartialFailure<Fail>;

export type Result<Data, Fail> =
| (Success<Data> & Transform<Data, Fail>)
| (Failure<Fail> & Transform<Data, Fail>);
export type Result<Data, Fail> = (
| (Data | true extends true ? never : Success<Data>)
| (Fail | true extends true ? never : Failure<Fail>)
) &
Transform<Data, Fail>;

export type Response<Data, Fail> = Promise<Result<Data, Fail>>;
8 changes: 4 additions & 4 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,10 @@ export const complete = <Data, Fail>(partial: PartialResult<Data, Fail>): Result
return Object.defineProperties(result, propsDefs) as Result<Data, Fail>;
};

export const ok = <Data = never>(
export const ok = <Data = never, Fail = never>(
data: Data,
{ code, order, skip }: OkMessage = {}
): Success<Data> => {
): Result<Data, Fail> => {
if (skip) {
order = -Infinity;
}
Expand Down Expand Up @@ -196,10 +196,10 @@ export const err = <Fail = never>(
return complete(exception) as Failure<Fail>;
};

export const fail = <Fail extends Err | undefined = never>(
export const fail = <Fail extends Err | undefined = never, Data = never>(
type: Fail extends Err ? Fail['type'] : undefined,
{ message, code, order, skip, ...error }: ErrorMessage<Fail> = {} as ErrorMessage<Fail>
): Failure<Fail> => {
): Result<Data, Fail> => {
const failure = ({
...error,
type,
Expand Down

0 comments on commit f44e748

Please sign in to comment.