Skip to content

Commit 1db9a01

Browse files
authored
Merge pull request #166 from Mitch5000/Add-typed-Result-helpers-for-pattern-matching-on-errors-FIXED
Add typed Result helpers for pattern matching on errors FIXED
2 parents 5f4a3fb + e9cf66f commit 1db9a01

3 files changed

Lines changed: 176 additions & 4 deletions

File tree

src/index.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,12 +196,22 @@ export {
196196
assertOk,
197197
attachTraceId,
198198
err,
199+
isAccountNotFound,
200+
isContractError,
199201
isErr,
200202
isErrorCode,
201203
isOk,
204+
isTxFailed,
202205
ok,
203206
} from "./shared/response";
204-
export type { SorokitError, SorokitResult } from "./shared/response";
207+
export type {
208+
AccountNotFoundErrorCode,
209+
ContractErrorCode,
210+
SorokitError,
211+
SorokitErrorResult,
212+
SorokitResult,
213+
TxFailedErrorCode,
214+
} from "./shared/response";
205215
export { generateTraceId } from "./shared/utils";
206216

207217
// ─── Metrics ──────────────────────────────────────────────────────────────────

src/shared/response.ts

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,25 @@ export interface SorokitError {
2626
traceId?: string;
2727
}
2828

29+
export type SorokitErrorResult<C extends SorokitErrorCode = SorokitErrorCode> = {
30+
status: "error";
31+
data: null;
32+
error: SorokitError & { code: C };
33+
};
34+
35+
export type AccountNotFoundErrorCode = SorokitErrorCode.ACCOUNT_NOT_FOUND;
36+
37+
export type TxFailedErrorCode =
38+
| SorokitErrorCode.TX_BUILD_FAILED
39+
| SorokitErrorCode.TX_SIMULATE_FAILED
40+
| SorokitErrorCode.TX_SUBMIT_FAILED;
41+
42+
export type ContractErrorCode =
43+
| SorokitErrorCode.CONTRACT_INVOKE_FAILED
44+
| SorokitErrorCode.CONTRACT_READ_FAILED
45+
| SorokitErrorCode.CONTRACT_PREPARE_FAILED
46+
| SorokitErrorCode.CONTRACT_SIMULATE_FAILED;
47+
2948
export enum SorokitErrorCode {
3049
// Wallet
3150
WALLET_NOT_FOUND = "WALLET_NOT_FOUND",
@@ -117,10 +136,42 @@ export function isErr<T>(
117136
export function isErrorCode<T, C extends SorokitErrorCode>(
118137
result: SorokitResult<T>,
119138
code: C,
120-
): result is { status: "error"; data: null; error: SorokitError & { code: C } } {
139+
): result is SorokitErrorResult<C> {
121140
return result.status === "error" && result.error.code === code;
122141
}
123142

143+
/** Type guard — narrows to ACCOUNT_NOT_FOUND error results. */
144+
export function isAccountNotFound<T>(
145+
result: SorokitResult<T>,
146+
): result is SorokitErrorResult<AccountNotFoundErrorCode> {
147+
return isErrorCode(result, SorokitErrorCode.ACCOUNT_NOT_FOUND);
148+
}
149+
150+
/** Type guard — narrows to transaction failure error results. */
151+
export function isTxFailed<T>(
152+
result: SorokitResult<T>,
153+
): result is SorokitErrorResult<TxFailedErrorCode> {
154+
return (
155+
result.status === "error" &&
156+
(result.error.code === SorokitErrorCode.TX_BUILD_FAILED ||
157+
result.error.code === SorokitErrorCode.TX_SIMULATE_FAILED ||
158+
result.error.code === SorokitErrorCode.TX_SUBMIT_FAILED)
159+
);
160+
}
161+
162+
/** Type guard — narrows to contract-related error results. */
163+
export function isContractError<T>(
164+
result: SorokitResult<T>,
165+
): result is SorokitErrorResult<ContractErrorCode> {
166+
return (
167+
result.status === "error" &&
168+
(result.error.code === SorokitErrorCode.CONTRACT_INVOKE_FAILED ||
169+
result.error.code === SorokitErrorCode.CONTRACT_READ_FAILED ||
170+
result.error.code === SorokitErrorCode.CONTRACT_PREPARE_FAILED ||
171+
result.error.code === SorokitErrorCode.CONTRACT_SIMULATE_FAILED)
172+
);
173+
}
174+
124175
/**
125176
* Assert that a result is ok — throws if it is an error.
126177
* Use in contexts where an error is unrecoverable and you want to fail fast.

src/tests/result.test.ts

Lines changed: 113 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,22 @@
1-
import { describe, it, expect } from "vitest";
2-
import { ok, err, isOk, isErr, isErrorCode, assertOk, SorokitErrorCode } from "../shared/response";
1+
import { describe, it, expect, expectTypeOf } from "vitest";
2+
import {
3+
ok,
4+
err,
5+
isOk,
6+
isErr,
7+
isErrorCode,
8+
isAccountNotFound,
9+
isTxFailed,
10+
isContractError,
11+
assertOk,
12+
SorokitErrorCode,
13+
} from "../shared/response";
14+
import type {
15+
AccountNotFoundErrorCode,
16+
ContractErrorCode,
17+
SorokitErrorResult,
18+
TxFailedErrorCode,
19+
} from "../shared/response";
320

421
describe("shared/response", () => {
522
describe("ok()", () => {
@@ -100,3 +117,97 @@ describe("assertOk()", () => {
100117
);
101118
});
102119
});
120+
121+
describe("error-specific result type guards", () => {
122+
describe("isAccountNotFound()", () => {
123+
it("returns true for ACCOUNT_NOT_FOUND", () => {
124+
const result = err(SorokitErrorCode.ACCOUNT_NOT_FOUND, "not found");
125+
expect(isAccountNotFound(result)).toBe(true);
126+
});
127+
128+
it("returns false for ok results and other error codes", () => {
129+
expect(isAccountNotFound(ok(42))).toBe(false);
130+
expect(
131+
isAccountNotFound(err(SorokitErrorCode.ACCOUNT_FETCH_FAILED, "fetch failed")),
132+
).toBe(false);
133+
expect(isAccountNotFound(err(SorokitErrorCode.TX_SUBMIT_FAILED, "tx failed"))).toBe(
134+
false,
135+
);
136+
});
137+
138+
it("narrows to the error branch with ACCOUNT_NOT_FOUND code", () => {
139+
const result = err<number>(SorokitErrorCode.ACCOUNT_NOT_FOUND, "not found");
140+
141+
if (isAccountNotFound(result)) {
142+
expectTypeOf(result).toEqualTypeOf<SorokitErrorResult<AccountNotFoundErrorCode>>();
143+
expectTypeOf(result.error.code).toEqualTypeOf<SorokitErrorCode.ACCOUNT_NOT_FOUND>();
144+
expect(result.data).toBeNull();
145+
}
146+
});
147+
});
148+
149+
describe("isTxFailed()", () => {
150+
it.each([
151+
SorokitErrorCode.TX_BUILD_FAILED,
152+
SorokitErrorCode.TX_SIMULATE_FAILED,
153+
SorokitErrorCode.TX_SUBMIT_FAILED,
154+
])("returns true for %s", (code) => {
155+
expect(isTxFailed(err(code, "transaction failed"))).toBe(true);
156+
});
157+
158+
it("returns false for ok results and non-transaction error codes", () => {
159+
expect(isTxFailed(ok(42))).toBe(false);
160+
expect(isTxFailed(err(SorokitErrorCode.TX_NOT_FOUND, "tx not found"))).toBe(false);
161+
expect(isTxFailed(err(SorokitErrorCode.ACCOUNT_NOT_FOUND, "not found"))).toBe(false);
162+
expect(isTxFailed(err(SorokitErrorCode.CONTRACT_INVOKE_FAILED, "contract"))).toBe(
163+
false,
164+
);
165+
});
166+
167+
it("narrows to the error branch with transaction failure codes", () => {
168+
const result = err<string>(SorokitErrorCode.TX_BUILD_FAILED, "build failed");
169+
170+
if (isTxFailed(result)) {
171+
expectTypeOf(result).toEqualTypeOf<SorokitErrorResult<TxFailedErrorCode>>();
172+
expectTypeOf(result.error.code).toEqualTypeOf<TxFailedErrorCode>();
173+
expect(result.data).toBeNull();
174+
}
175+
});
176+
});
177+
178+
describe("isContractError()", () => {
179+
it.each([
180+
SorokitErrorCode.CONTRACT_INVOKE_FAILED,
181+
SorokitErrorCode.CONTRACT_READ_FAILED,
182+
SorokitErrorCode.CONTRACT_PREPARE_FAILED,
183+
SorokitErrorCode.CONTRACT_SIMULATE_FAILED,
184+
])("returns true for %s", (code) => {
185+
expect(isContractError(err(code, "contract failed"))).toBe(true);
186+
});
187+
188+
it("returns false for ok results and non-contract error codes", () => {
189+
expect(isContractError(ok(42))).toBe(false);
190+
expect(isContractError(err(SorokitErrorCode.TX_SUBMIT_FAILED, "tx failed"))).toBe(
191+
false,
192+
);
193+
expect(isContractError(err(SorokitErrorCode.ACCOUNT_NOT_FOUND, "not found"))).toBe(
194+
false,
195+
);
196+
expect(isContractError(err(SorokitErrorCode.NETWORK_ERROR, "network"))).toBe(false);
197+
});
198+
199+
it("narrows to the error branch with contract error codes", () => {
200+
const result = err<boolean>(
201+
SorokitErrorCode.CONTRACT_SIMULATE_FAILED,
202+
"simulate failed",
203+
);
204+
205+
if (isContractError(result)) {
206+
expectTypeOf(result).toEqualTypeOf<SorokitErrorResult<ContractErrorCode>>();
207+
expectTypeOf(result.error.code).toEqualTypeOf<ContractErrorCode>();
208+
expect(result.data).toBeNull();
209+
}
210+
});
211+
});
212+
});
213+

0 commit comments

Comments
 (0)