Serialization & Interop
@hex-di/result provides built-in serialization support and type utilities for integration with other systems.
JSON Serialization
toJSON() Method
All Result instances can be serialized to JSON:
import { ok, err } from "@hex-di/result";
const success = ok({ id: 1, name: "Alice" });
const json = success.toJSON();
// {
// _tag: "Ok",
// _schemaVersion: 1,
// value: { id: 1, name: 'Alice' }
// }
const failure = err("Not found");
const errorJson = failure.toJSON();
// {
// _tag: "Err",
// _schemaVersion: 1,
// error: "Not found"
// }
fromJSON(json) Deserialization
Deserialize JSON back to Result:
import { fromJSON } from "@hex-di/result";
const json = {
_tag: "Ok",
_schemaVersion: 1,
value: 42,
};
const result = fromJSON(json); // Ok(42)
// Error case
const errorJson = {
_tag: "Err",
_schemaVersion: 1,
error: "Failed",
};
const error = fromJSON(errorJson); // Err("Failed")
Option Serialization
import { some, none, fromOptionJSON } from "@hex-di/result";
// Serialize
const someValue = some(42);
const someJson = someValue.toJSON();
// { _tag: 'Some', value: 42 }
const noneValue = none();
const noneJson = noneValue.toJSON();
// { _tag: 'None' }
// Deserialize
const restored = fromOptionJSON({ _tag: "Some", value: 42 });
// some(42)
Standard Schema v1
toSchema(result)
Convert Results to Standard Schema v1 format for validation libraries:
import { ok, toSchema } from "@hex-di/result";
const result = ok({ id: 1, name: "Alice" });
const schema = toSchema(result);
// Use with validation libraries that support Standard Schema
Type Utilities
Extracting Types
InferOk<R> and InferErr<R>
Extract the Ok or Err type from a Result:
import type { Result, InferOk, InferErr } from "@hex-di/result";
type UserResult = Result<User, string>;
type UserType = InferOk<UserResult>; // User
type ErrorType = InferErr<UserResult>; // string
InferAsyncOk<R> and InferAsyncErr<R>
Extract types from ResultAsync:
import type { ResultAsync, InferAsyncOk, InferAsyncErr } from "@hex-di/result";
type AsyncUserResult = ResultAsync<User, ApiError>;
type UserType = InferAsyncOk<AsyncUserResult>; // User
type ErrorType = InferAsyncErr<AsyncUserResult>; // ApiError
Type Checking
IsResult<R>
Check if a type is a Result at the type level:
import type { Result, IsResult } from "@hex-di/result";
type Test1 = IsResult<Result<number, string>>; // true
type Test2 = IsResult<number>; // false
type Test3 = IsResult<Promise<number>>; // false
Type Transformations
FlattenResult<R>
Flatten nested Results:
import type { Result, FlattenResult } from "@hex-di/result";
type Nested = Result<Result<number, string>, Error>;
type Flat = FlattenResult<Nested>; // Result<number, string | Error>
InferOkTuple<Results> and InferErrUnion<Results>
Work with tuples of Results:
import type { Result, InferOkTuple, InferErrUnion } from "@hex-di/result";
type Results = [Result<number, string>, Result<User, ApiError>, Result<boolean, ValidationError>];
type OkTypes = InferOkTuple<Results>; // [number, User, boolean]
type ErrTypes = InferErrUnion<Results>; // string | ApiError | ValidationError
Integration with GraphBuilder
@hex-di/graph's tryBuild() returns a Result:
import { GraphBuilder } from "@hex-di/graph";
import { createContainer } from "@hex-di/core";
import type { Result } from "@hex-di/result";
const result = GraphBuilder.create().provide(LoggerAdapter).provide(UserServiceAdapter).tryBuild();
if (result.isErr()) {
console.error("Graph build failed:", result.error.message);
// Handle specific error types
switch (result.error.type) {
case "CIRCULAR_DEPENDENCY":
console.error("Circular dependency detected");
break;
case "MISSING_DEPENDENCY":
console.error("Missing required dependency");
break;
default:
console.error("Unknown build error");
}
process.exit(1);
}
// Type-safe access to the graph
const container = createContainer({
graph: result.value,
name: "App",
});
API Response Handling
Express.js Integration
import { type Result } from "@hex-di/result";
import express from "express";
class ApiController {
async getUser(req: express.Request, res: express.Response) {
const result = await this.userService.getUser(req.params.id);
result.match(
user => res.json({ success: true, data: user }),
error => {
const status = this.errorToStatus(error);
res.status(status).json({
success: false,
error: error._tag,
message: error.message,
});
}
);
}
private errorToStatus(error: AppError): number {
switch (error._tag) {
case "NotFound":
return 404;
case "Unauthorized":
return 401;
case "Validation":
return 400;
default:
return 500;
}
}
}
GraphQL Integration
import { type Result } from "@hex-di/result";
const resolvers = {
Query: {
user: async (_, { id }, context) => {
const result = await context.userService.getUser(id);
return result.match(
user => user,
error => {
throw new GraphQLError(error.message, {
extensions: {
code: error._tag,
details: error.data,
},
});
}
);
},
},
Mutation: {
createUser: async (_, { input }, context) => {
const result = await context.userService.createUser(input);
if (result.isErr()) {
return {
__typename: "CreateUserError",
code: result.error._tag,
message: result.error.message,
};
}
return {
__typename: "CreateUserSuccess",
user: result.value,
};
},
},
};
Database Transaction Example
import { safeTry, ok, err, type Result } from "@hex-di/result";
class OrderRepository {
async createOrderWithItems(
order: Order,
items: OrderItem[]
): Promise<Result<string, DatabaseError>> {
const trx = await this.db.transaction();
return safeTry(
function* () {
// Insert order
const orderId = yield* this.insertOrder(trx, order).mapErr(
e => new DatabaseError("Failed to insert order", e)
);
// Insert items
for (const item of items) {
yield* this.insertOrderItem(trx, orderId, item).mapErr(
e => new DatabaseError(`Failed to insert item ${item.id}`, e)
);
}
// Update inventory
yield* this.updateInventory(trx, items).mapErr(
e => new DatabaseError("Failed to update inventory", e)
);
// Commit transaction
yield* this.commitTransaction(trx).mapErr(
e => new DatabaseError("Failed to commit transaction", e)
);
return ok(orderId);
}.bind(this)
).orElse(async error => {
// Rollback on any error
await trx.rollback();
return err(error);
});
}
}
Testing with Results
import { ok, err, type Result } from "@hex-di/result";
import { describe, it, expect } from "vitest";
describe("UserService", () => {
it("should return user when found", async () => {
const service = new UserService(mockDb);
const result = await service.getUser("123");
expect(result.isOk()).toBe(true);
expect(result.unwrapOr(null)).toEqual({
id: "123",
name: "Alice",
});
});
it("should return error when user not found", async () => {
const service = new UserService(mockDb);
const result = await service.getUser("unknown");
expect(result.isErr()).toBe(true);
expect(result.isErrAnd(e => e._tag === "NotFound")).toBe(true);
});
it("should accumulate validation errors", () => {
const results = validateForm({
email: "invalid",
password: "123",
age: -1,
});
expect(results.isErr()).toBe(true);
expect(results.error).toEqual([
"Invalid email format",
"Password too short",
"Age must be positive",
]);
});
});
Migration Guide
From try-catch to Result
Before:
async function fetchUser(id: string): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return await response.json();
} catch (error) {
console.error("Failed to fetch user:", error);
throw error;
}
}
After:
import { fromPromise, err, type Result } from "@hex-di/result";
async function fetchUser(id: string): Promise<Result<User, ApiError>> {
return fromPromise(fetch(`/api/users/${id}`), e => ({
type: "NetworkError",
message: String(e),
})).andThen(response => {
if (!response.ok) {
return err({
type: "HttpError",
status: response.status,
});
}
return fromPromise(response.json() as Promise<User>, () => ({
type: "ParseError",
message: "Invalid JSON",
}));
});
}