Skip to content

Commit 93a59a1

Browse files
committed
adds toTuple method and util infer getters
1 parent 15d34ed commit 93a59a1

File tree

4 files changed

+246
-1
lines changed

4 files changed

+246
-1
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
{
22
"private": false,
33
"name": "typescript-result",
4-
"version": "3.0.0",
4+
"version": "3.1.0",
55
"description": "A Result type inspired by Rust and Kotlin that leverages TypeScript's powerful type system to simplify error handling and make your code more readable and maintainable.",
66
"keywords": [
77
"result",

readme.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -797,6 +797,27 @@ if (result.isError()) {
797797
}
798798
```
799799

800+
### toTuple()
801+
802+
**returns** the result in a tuple format where the first element is the value and the second element is the error.
803+
804+
If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`.
805+
This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities.
806+
807+
#### Example
808+
Narrowing down the type using destructuring
809+
```ts
810+
declare const result: Result<number, ErrorA>;
811+
812+
const [value, error] = result.toTuple();
813+
814+
if (error) {
815+
// error is ErrorA
816+
} else {
817+
// at this point the value must be a number
818+
}
819+
```
820+
800821
### errorOrNull()
801822

802823
**returns** the encapsulated error if the result is a failure, otherwise `null`.
@@ -1316,6 +1337,27 @@ class AsyncResult<Value, Error> {}
13161337

13171338
Utility getter that checks if the current instance is an `AsyncResult`.
13181339

1340+
### toTuple()
1341+
1342+
**returns** the result in a tuple format where the first element is the value and the second element is the error.
1343+
1344+
If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`.
1345+
This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities.
1346+
1347+
#### Example
1348+
Narrowing down the type using destructuring
1349+
```ts
1350+
declare const result: AsyncResult<number, ErrorA>;
1351+
1352+
const [value, error] = result.toTuple();
1353+
1354+
if (error) {
1355+
// error is ErrorA
1356+
} else {
1357+
// at this point the value must be a number
1358+
}
1359+
```
1360+
13191361
### errorOrNull()
13201362

13211363
**returns** the encapsulated error if the result is a failure, otherwise `null`.

src/result.test.ts

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -611,6 +611,15 @@ describe("Result", () => {
611611
});
612612

613613
describe("instance methods and getters", () => {
614+
describe("$inferValue / $inferError", () => {
615+
it("infers the value and error type of a result", () => {
616+
const result: Result<number, ErrorA> = Result.ok(42);
617+
618+
expectTypeOf(result.$inferValue).toEqualTypeOf<number>();
619+
expectTypeOf(result.$inferError).toEqualTypeOf<ErrorA>();
620+
});
621+
});
622+
614623
describe("value", () => {
615624
it("returns the encapsulated value on success", () => {
616625
const result: Result<number, ErrorA> = Result.ok(42);
@@ -723,6 +732,56 @@ describe("Result", () => {
723732
});
724733
});
725734

735+
describe("toTuple", () => {
736+
it("returns a tuple on a successful result", () => {
737+
const result: Result<number, ErrorA> = Result.ok(2);
738+
739+
const [value, error] = result.toTuple();
740+
if (error) {
741+
expectTypeOf(error).toEqualTypeOf<ErrorA>();
742+
} else {
743+
expectTypeOf(value).toEqualTypeOf<number>();
744+
}
745+
746+
expect(value).toBe(2);
747+
expect(error).toBeNull();
748+
});
749+
750+
it("returns a tuple on a failed result", () => {
751+
const result: Result<number, ErrorA> = Result.error(errorA);
752+
753+
const [value, error] = result.toTuple();
754+
if (error) {
755+
expectTypeOf(error).toEqualTypeOf<ErrorA>();
756+
} else {
757+
expectTypeOf(value).toEqualTypeOf<number>();
758+
}
759+
760+
expect(value).toBeNull();
761+
expect(error).toBe(errorA);
762+
});
763+
764+
it("handles cases where the result can only be successful", () => {
765+
const result = Result.ok(12);
766+
767+
const [value, error] = result.toTuple();
768+
expectTypeOf(value).toEqualTypeOf<number>();
769+
expectTypeOf(error).toEqualTypeOf<never>();
770+
expect(value).toBe(12);
771+
expect(error).toBe(null);
772+
});
773+
774+
it("handles cases where the result can only be a failure", () => {
775+
const result = Result.error(errorA);
776+
777+
const [value, error] = result.toTuple();
778+
expectTypeOf(value).toEqualTypeOf<never>();
779+
expectTypeOf(error).toEqualTypeOf<ErrorA>();
780+
expect(value).toBe(null);
781+
expect(error).toBe(errorA);
782+
});
783+
});
784+
726785
describe("errorOrNull", () => {
727786
it("returns the error on failure", () => {
728787
const result: Result<number, CustomError> = Result.error(
@@ -1605,13 +1664,72 @@ describe("AsyncResult", () => {
16051664
});
16061665

16071666
describe("instance methods and getters", () => {
1667+
describe("$inferValue / $inferError", () => {
1668+
it("infers the value and error type of a result", () => {
1669+
const result: AsyncResult<number, ErrorA> = AsyncResult.ok(42);
1670+
1671+
expectTypeOf(result.$inferValue).toEqualTypeOf<number>();
1672+
expectTypeOf(result.$inferError).toEqualTypeOf<ErrorA>();
1673+
});
1674+
});
1675+
16081676
describe("isAsyncResult", () => {
16091677
it("tests whether a value is an async-result", () => {
16101678
const asyncResult = AsyncResult.ok(12);
16111679
expect(asyncResult.isAsyncResult).toBe(true);
16121680
});
16131681
});
16141682

1683+
describe("toTuple", () => {
1684+
it("returns a tuple on a successful result", async () => {
1685+
const result: AsyncResult<number, ErrorA> = AsyncResult.ok(2);
1686+
1687+
const [value, error] = await result.toTuple();
1688+
if (error) {
1689+
expectTypeOf(error).toEqualTypeOf<ErrorA>();
1690+
} else {
1691+
expectTypeOf(value).toEqualTypeOf<number>();
1692+
}
1693+
1694+
expect(value).toBe(2);
1695+
expect(error).toBeNull();
1696+
});
1697+
1698+
it("returns a tuple on a failed result", async () => {
1699+
const result: AsyncResult<number, ErrorA> = AsyncResult.error(errorA);
1700+
1701+
const [value, error] = await result.toTuple();
1702+
if (error) {
1703+
expectTypeOf(error).toEqualTypeOf<ErrorA>();
1704+
} else {
1705+
expectTypeOf(value).toEqualTypeOf<number>();
1706+
}
1707+
1708+
expect(value).toBeNull();
1709+
expect(error).toBe(errorA);
1710+
});
1711+
1712+
it("handles cases where the result can only be successful", async () => {
1713+
const result = AsyncResult.ok(12);
1714+
1715+
const [value, error] = await result.toTuple();
1716+
expectTypeOf(value).toEqualTypeOf<number>();
1717+
expectTypeOf(error).toEqualTypeOf<never>();
1718+
expect(value).toBe(12);
1719+
expect(error).toBe(null);
1720+
});
1721+
1722+
it("handles cases where the result can only be a failure", async () => {
1723+
const result = AsyncResult.error(errorA);
1724+
1725+
const [value, error] = await result.toTuple();
1726+
expectTypeOf(value).toEqualTypeOf<never>();
1727+
expectTypeOf(error).toEqualTypeOf<ErrorA>();
1728+
expect(value).toBe(null);
1729+
expect(error).toBe(errorA);
1730+
});
1731+
});
1732+
16151733
describe("errorOrNull", async () => {
16161734
it("returns the error on failure", async () => {
16171735
const asyncResult = AsyncResult.error(errorA) as AsyncResult<

src/result.ts

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import type {
1515
} from "./helpers.js";
1616
import { isAsyncFn, isFunction, isPromise } from "./helpers.js";
1717

18+
// TODO: also add transformError fn to regular map function??
19+
1820
type InferError<T> = T extends Result<any, infer Error> ? Error : never;
1921
type InferValue<T> = T extends Result<infer Value, any> ? Value : T;
2022

@@ -51,13 +53,56 @@ type AccountForFunctionThrowing<Items extends any[]> =
5153
* Represents the asynchronous outcome of an operation that can either succeed or fail.
5254
*/
5355
export class AsyncResult<Value, Err> extends Promise<Result<Value, Err>> {
56+
/**
57+
* Utiltity getter to infer the value type of the result.
58+
* Note: this getter does not hold any value, it's only used for type inference.
59+
*/
60+
declare $inferValue: Value;
61+
62+
/**
63+
* Utiltity getter to infer the error type of the result.
64+
* Note: this getter does not hold any value, it's only used for type inference.
65+
*/
66+
declare $inferError: Err;
67+
5468
/**
5569
* Utility getter to check if the current instance is an `AsyncResult`.
5670
*/
5771
get isAsyncResult(): true {
5872
return true;
5973
}
6074

75+
/**
76+
* @returns the result in a tuple format where the first element is the value and the second element is the error.
77+
* If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`.
78+
*
79+
* This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities.
80+
*
81+
* @example Narrowing down the result type using destructuring
82+
* ```ts
83+
* declare const result: AsyncResult<number, ErrorA>;
84+
*
85+
* const [value, error] = await result.toTuple();
86+
*
87+
* if (error) {
88+
* // error is ErrorA
89+
* return;
90+
* }
91+
*
92+
* // value must be a number
93+
* ```
94+
*/
95+
async toTuple(): Promise<
96+
[Err] extends [never]
97+
? [value: Value, error: never]
98+
: [Value] extends [never]
99+
? [value: never, error: Err]
100+
: [value: Value, error: null] | [value: null, error: Err]
101+
> {
102+
const result = await this;
103+
return result.toTuple();
104+
}
105+
61106
/**
62107
* @returns the encapsulated error if the result is a failure, otherwise `null`.
63108
*/
@@ -549,6 +594,18 @@ export class Result<Value, Err> {
549594
private readonly _error: Err,
550595
) {}
551596

597+
/**
598+
* Utiltity getter to infer the value type of the result.
599+
* Note: this getter does not hold any value, it's only used for type inference.
600+
*/
601+
declare $inferValue: Value;
602+
603+
/**
604+
* Utiltity getter to infer the error type of the result.
605+
* Note: this getter does not hold any value, it's only used for type inference.
606+
*/
607+
declare $inferError: Err;
608+
552609
/**
553610
* Utility getter that checks if the current instance is a `Result`.
554611
*/
@@ -661,6 +718,34 @@ export class Result<Value, Err> {
661718
return this.failure;
662719
}
663720

721+
/**
722+
* @returns the result in a tuple format where the first element is the value and the second element is the error.
723+
* If the result is successful, the error will be `null`. If the result is a failure, the value will be `null`.
724+
*
725+
* This method is especially useful when you want to destructure the result into a tuple and use TypeScript's narrowing capabilities.
726+
*
727+
* @example Narrowing down the result type using destructuring
728+
* ```ts
729+
* declare const result: Result<number, ErrorA>;
730+
*
731+
* const [value, error] = result.toTuple();
732+
*
733+
* if (error) {
734+
* // error is ErrorA
735+
* return;
736+
* }
737+
*
738+
* // value must be a number
739+
* ```
740+
*/
741+
toTuple() {
742+
return [this._value ?? null, this._error ?? null] as [Err] extends [never]
743+
? [value: Value, error: never]
744+
: [Value] extends [never]
745+
? [value: never, error: Err]
746+
: [value: Value, error: null] | [value: null, error: Err];
747+
}
748+
664749
/**
665750
* @returns the encapsulated error if the result is a failure, otherwise `null`.
666751
*/

0 commit comments

Comments
 (0)