Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
284 changes: 284 additions & 0 deletions src/result.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1198,6 +1198,147 @@ describe("Result", () => {
});
});

describe("mapError", () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see that you use CustomError a lot in the added test cases. At first glance there is nothing particularly wrong with this, although type-wise it tend to 'slip through the cracks' here and there. Are you ok with using the ErrorA and ErrorB classes to proof things like transformations of errors, etc? As bonus you only have to match against the constructor of the error.

it("maps an encapsulated error value to a next result using a transform function", () => {
const result: Result<number, CustomError> = Result.error(
new CustomError("TEST_ERROR"),
);
const nextResult = result.mapError(
(error) => new CustomError(`FROM_${error.message}`),
);
expectTypeOf(nextResult).toEqualTypeOf<Result<number, CustomError>>();
Result.assertError(nextResult);
expect(nextResult.error).toBeInstanceOf(CustomError);
expect(nextResult.error.message).toBe("FROM_TEST_ERROR");
expect(result).not.toBe(nextResult);
});

it("maps an encapsulated error value to an async-result using an async transform function", async () => {
const result: Result<number, CustomError> = Result.error(
new CustomError("TEST_ERROR"),
);
const nextAsyncResult = result.mapError(
async (error) => new CustomError(`FROM_${error.message}`),
);
expectTypeOf(nextAsyncResult).toEqualTypeOf<
AsyncResult<number, CustomError>
>();
expect(nextAsyncResult).toBeInstanceOf(AsyncResult);

const nextResult = await nextAsyncResult;

Result.assertError(nextResult);
expect(nextResult.error).toBeInstanceOf(CustomError);
expect(nextResult.error.message).toBe("FROM_TEST_ERROR");
});

it("lets you map over an encapsulated succes value by simply ignoring the transform function and returning the success result", () => {
const result = Result.ok(2) as Result<number, CustomError>;
const nextResult = result.mapError(
(error) => new CustomError("TEST_ERROR", { cause: error }),
);

expectTypeOf(nextResult).toEqualTypeOf<Result<number, CustomError>>();
expect(result).toBe(nextResult);
Result.assertOk(nextResult);
expect(nextResult.value).toEqual(2);
});

it("accounts for the async transform function even when it is a failed result", async () => {
const result = Result.ok(2) as Result<number, CustomError>;
const nextAsyncResult = result.mapError(
async (error) => new CustomError("TEST_ERROR", { cause: error }),
);

expectTypeOf(nextAsyncResult).toEqualTypeOf<
AsyncResult<number, CustomError>
>();
expect(nextAsyncResult).toBeInstanceOf(AsyncResult);

const nextResult = await nextAsyncResult;
Result.assertOk(nextResult);
expect(nextResult.value).toEqual(2);
});

it("flattens a returning result from the transformation", () => {
const result: Result<number, CustomError> = Result.error(
new CustomError("INNER_ERROR"),
);
const nextResult = result.mapError((error) =>
Result.error(new CustomError("TEST_ERROR", { cause: error })),
);
expectTypeOf(nextResult).toEqualTypeOf<Result<number, CustomError>>();
Result.assertError(nextResult);
expect(nextResult.error.message).toBe("TEST_ERROR");
expect(result).not.toBe(nextResult);
});

it("flattens a returning result from the async transformation", async () => {
const result: Result<number, CustomError> = Result.error(
new CustomError("INNER_ERROR"),
);
const nextAsyncResult = result.mapError(async (error) =>
Result.error(new CustomError("TEST_ERROR", { cause: error })),
);

expectTypeOf(nextAsyncResult).toEqualTypeOf<
AsyncResult<number, CustomError>
>();

const nextResult = await nextAsyncResult;

Result.assertError(nextResult);
expect(nextResult.error.message).toBe("TEST_ERROR");
expect(result).not.toBe(nextResult);
});

it("flattens a returning async-result from the transformation", async () => {
const result: Result<number, CustomError> = Result.error(
new CustomError("INNER_ERROR"),
);
const otherAsyncResult = Result.fromAsyncCatching(
Promise.reject(new CustomError("OTHER_ERROR")),
);

const nextAsyncResult = result.mapError(() => otherAsyncResult);
expectTypeOf(nextAsyncResult).toEqualTypeOf<
AsyncResult<number, CustomError>
>();

const nextResult = await nextAsyncResult;

Result.assertError(nextResult);
expect(nextResult.error.message).toBe("OTHER_ERROR");
expect(result).not.toBe(nextResult);
});

it("does not track errors thrown inside the transformation function", () => {
expect(() =>
Result.error(new CustomError()).mapError((_error) => {
throw new CustomError("THROWN_ERROR");
}),
).to.throw(CustomError);
});

it("will convert a success into an async-result when an async transform function was given", async () => {
const result = Result.ok(2) as Result<number, CustomError>;

const asyncResult = result.mapError(
async (_error) => new CustomError("TEST_ERROR"),
);

expectTypeOf(asyncResult).toEqualTypeOf<
AsyncResult<number, CustomError>
>();

expect(asyncResult).toBeInstanceOf(AsyncResult);

const resolvedAsyncResult = await asyncResult;
Result.assertOk(resolvedAsyncResult);
expect(resolvedAsyncResult.value).toBe(2);
});
});

describe("mapCatching", () => {
it("does track errors thrown inside the transformation function", () => {
const fn = () =>
Expand Down Expand Up @@ -1235,6 +1376,22 @@ describe("Result", () => {
Result.assertError(nextResult);
expect(spy).not.toHaveBeenCalled();
});

it("transforms thrown errors when an errorTransform function is defined", async () => {
const result: Result<number, CustomError> = await Result.ok(
2,
).mapCatching(
async (): Promise<number> => {
throw new Error("TEST_ERROR");
},
(_error) => {
return new CustomError();
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is an example of where CustomError creates a false positive.
If you replace this with ErrorA it will probably fail the typecheck. See comment about types of mapCatching in result.ts.

},
);

Result.assertError(result);
expect(result.error).toBeInstanceOf(CustomError);
});
});
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test for when an exception gets thrown inside the errorTransform fn?


describe("recover", () => {
Expand Down Expand Up @@ -1961,6 +2118,119 @@ describe("AsyncResult", () => {
});
});

describe("mapError", () => {
it("maps an encapsulated error value to a next result using a transform function", async () => {
const result: AsyncResult<number, CustomError> = AsyncResult.error(
new CustomError("TEST_ERROR"),
);
const nextResult = result.mapError(
(error) => new CustomError("MAPPED_ERROR", { cause: error }),
);
expectTypeOf(nextResult).toEqualTypeOf<
AsyncResult<number, CustomError>
>();
const resolvedResult = await nextResult;
Result.assertError(resolvedResult);
expect(resolvedResult.error.message).toBe("MAPPED_ERROR");
});

it("maps an encapsulated error value to an async-result using an async transform function", async () => {
const result = AsyncResult.error(new CustomError());
const nextAsyncResult = result.mapError(
async (error) => new CustomError("MAPPED_ERROR", { cause: error }),
);
expectTypeOf(nextAsyncResult).toEqualTypeOf<
AsyncResult<never, CustomError>
>();
expect(nextAsyncResult).toBeInstanceOf(AsyncResult);

const nextResult = await nextAsyncResult;

Result.assertError(nextResult);
expect(nextResult.error.message).toBe("MAPPED_ERROR");
});

it("lets you map over an encapsulated success value by simply ignoring the transform function and returning the success result", async () => {
const result = AsyncResult.ok(2) as AsyncResult<number, CustomError>;

const spy = vi.fn();
const nextResult = result.mapError((_error) => {
spy();
return new CustomError();
});

expectTypeOf(nextResult).toEqualTypeOf<
AsyncResult<number, CustomError>
>();
expect(spy).not.toHaveBeenCalled();

// Async result will always return a new instance
expect(result).not.toBe(nextResult);

const resolvedResult = await nextResult;
Result.assertOk(resolvedResult);
expect(resolvedResult.value).toEqual(2);
});

it("flattens a returning result from the transformation", async () => {
const result = AsyncResult.error(new CustomError("TEST_ERROR"));
const nextResult = result.mapError((error) =>
Result.error(new CustomError(`FROM_${error.message}`)),
);
expectTypeOf(nextResult).toEqualTypeOf<
AsyncResult<never, CustomError>
>();

const resolvedResult = await nextResult;
Result.assertError(resolvedResult);
expect(resolvedResult.error.message).toBe("FROM_TEST_ERROR");
expect(result).not.toBe(nextResult);
});

it("flattens a returning result from the async transformation", async () => {
const result = AsyncResult.error(new CustomError("TEST_ERROR"));
const nextAsyncResult = result.mapError(
async (error) => new CustomError(`FROM_${error.message}`),
);

expectTypeOf(nextAsyncResult).toEqualTypeOf<
AsyncResult<never, CustomError>
>();

const nextResult = await nextAsyncResult;

Result.assertError(nextResult);
expect(nextResult.error.message).toBe("FROM_TEST_ERROR");
expect(result).not.toBe(nextResult);
});

it("flattens a returning async-result from the transformation", async () => {
const result = AsyncResult.error(new CustomError("TEST_ERROR"));
const otherAsyncResult = Result.fromAsyncCatching(
Promise.reject(new CustomError("OTHER_ERROR")),
);

const nextAsyncResult = result.mapError(() => otherAsyncResult);
expectTypeOf(nextAsyncResult).toEqualTypeOf<
AsyncResult<never, CustomError>
>();

const nextResult = await nextAsyncResult;

Result.assertError(nextResult);
expect(nextResult.error.message).toBe("OTHER_ERROR");
expect(result).not.toBe(nextResult);
});

it("does not track errors thrown inside the transformation function", async () => {
await expect(() =>
AsyncResult.error(new CustomError()).mapError((): Error => {
throw new CustomError();
}),
).rejects.toThrow(CustomError);
});
});

describe("mapCatching", () => {
it("does track errors thrown inside the transformation function", async () => {
const result = await AsyncResult.ok(2).mapCatching((): number => {
Expand All @@ -1980,6 +2250,20 @@ describe("AsyncResult", () => {
Result.assertError(result);
expect(result.error).toBeInstanceOf(CustomError);
});

it("transforms thrown errors when an errorTransform function is defined", async () => {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the same comments of the sync-variant of mapCatching apply here as well

const result = await AsyncResult.ok(2).mapCatching(
async (): Promise<number> => {
throw new Error("TEST_ERROR");
},
(_error) => {
return new CustomError();
},
);

Result.assertError(result);
expect(result.error).toBeInstanceOf(CustomError);
});
});

describe("recover", () => {
Expand Down
Loading