Work with Results using Generator functions
masterResult type. This is an optional feature inspired by patterns used in EffectTS.repository·master·Indexed 19 days ago
https://github.com/everweij/typescript-resultA lightweight (2KB) library providing a type-safe Result type for TypeScript to replace try-catch blocks. It supports chaining and generator-based patterns via Result.gen(), pattern matching with .match(), and asynchronous operation handling through AsyncResult and Result.fromAsync. The library requires TypeScript 4.8.0+ and strict null checks to ensure full type safety.
Result type. This is an optional feature inspired by patterns used in EffectTS.Instead of a global mapError that might override unrelated errors, you can nest mapError calls inside a map block. This allows you to transform errors close to their source.
When you nest mapError inside a map, the resulting error type becomes a union of the original error type and the new error type produced by the nested transformation.
declare result: Result<string, ErrorA>;
declare otherResult: Result<string, ErrorB>;
const nextResult = result.map(value =>
otherResult.mapError(() => new ErrorC()) // Result<string, ErrorA | ErrorC>
);When working with asynchronous operations, typescript-result provides AsyncResult. This is essentially a Promise containing a Result. It allows you to chain operations like .map() without manually await-ing every intermediate step or nesting multiple if (result.ok) checks. The library automatically converts synchronous Result instances into AsyncResult when an async operation is introduced into the chain.
import { Result } from "typescript-result";
// The library automatically handles the transition from Result to AsyncResult
const result = await Result.ok(12)
// map the value to a Promise -> returns AsyncResult
.map((value) => Promise.resolve(value * 2))
// map async to another result -> returns AsyncResult
.map(async (value) => {
if (value < 10) {
return Result.error(new Error("Value too low"));
}
return Result.ok("All good!");
});When an async function returns a Result, it creates a "box within a box" pattern: Promise<Result<T, E>>. This requires nested unwrapping (e.g., await (await operation()).map(...)), which is unergonomic.
typescript-result provides the AsyncResult type to solve this. An AsyncResult is essentially a Promise that holds a Result, but it exposes the same functional methods as a regular Result (like map, toTuple, etc.), allowing you to chain operations without manual unwrapping.
// Standard Result becomes AsyncResult when mapped with an async function
const nextResult = result.map(async (value) => {
await sleep(1000);
return value.toUpperCase();
}); // Returns AsyncResult<string, Error>The map method is polymorphic and can change the type of the Result or AsyncResult based on the return value of the callback. Depending on what the callback returns, the chain can transition between different types:
Result type.Result.ok(...): Keeps the same Result type (due to automatic flattening).Promise or using an async function: Converts the Result into an AsyncResult.function*) with yield*: Converts the Result into an AsyncResult.import { Result } from "typescript-result";
declare function someOperation(): Result<number, Error>;
// ---cut-before---
declare const result: Result<number, Error>;
const nextResult = result // Result<number, Error>
.map((value) => value * 2) // Result<number, Error>
.map((value) => Result.ok(value * 2)) // Result<number, Error>
.map((value) => Promise.resolve(value * 2)) // AsyncResult<number, Error>
.map(async (value) => value * 2) // AsyncResult<number, Error>
.map(async (value) => Result.ok(value * 2)) // AsyncResult<number, Error>
.map(function* (value) {
const other = yield* someOperation();
return value * other;
}); // AsyncResult<number, Error>The typescript-result library follows the "errors-as-values" pattern (similar to Rust or Go).
Instead of throwing exceptions that disrupt the execution flow and are often difficult to track, you return a Result object. This approach provides several benefits:
UserNotFound) and unexpected system failures.A Result type is a container (or "box") that represents the outcome of an operation. It can hold one of two states:
T.E.Instead of using try/catch blocks which disrupt program flow, the Result type allows you to treat outcomes as data. This enables you to work with the contents of the result without manually unwrapping it at every step, providing a more predictable way to handle operations that might fail.
type Result<T, E> = {
ok: true;
value: T;
} | {
ok: false;
error: E;
};
declare function someOperation(): Result<string, Error>;Result.ok(value) or Result.error(error) and let the compiler infer the success and failure types based on the context.The match() method provides automatic exhaustive checks. If you fail to handle one of the possible error types defined in your Result union, TypeScript will report a compile-time error, ensuring all error cases are accounted for.
// @errors: 2349
import { Result } from "typescript-result";
class ErrorA extends Error { readonly type = "error-a"; }
class ErrorB extends Error { readonly type = "error-b"; }
// ---cut-before---
declare const result: Result<string, ErrorA | ErrorB>;
if (!result.ok) {
result
.match()
.when(ErrorA, (error) => console.error("Error A:", error.message))
.run(); // TypeScript error: ErrorB is not handled
} else {
console.log("Everything went fine:", result.value);
}If you are using a function that must return a value (like the callback in getOrElse), you can leverage TypeScript's noImplicitReturn compiler option to enforce exhaustive error handling.
When noImplicitReturn is enabled (which is automatic if strict is enabled), TypeScript will flag an error if a switch statement handling error types fails to provide a return statement for every possible case. This prevents accidental fall-through when a new error type is introduced to the union.
import { Result } from "typescript-result";
class ErrorA extends Error { readonly type = "error-a"; }
class ErrorB extends Error { readonly type = "error-b"; }
declare const result: Result<string, ErrorA | ErrorB>;
// If noImplicitReturn is enabled, this will error because ErrorB is not handled
const output = result.getOrElse((error) => {
switch (error.type) {
case "error-a":
return "Fallback for Error A";
// ErrorB is missing a return case, triggering a TS error
}
});To avoid repetitive if (!result.ok) return result; checks (the Go-style pattern), you can chain operations on a Result instance.
.map(fn): Transforms the successful value inside a Result. If the function returns another Result, the chain becomes polymorphic (nested Results)..mapCatching(fn, errorMapper): Similar to map, but allows the transformation function itself to throw. If it throws, the error is caught and transformed using the provided errorMapper.This allows you to build complex workflows where each step only executes if the previous one succeeded.
import { Result } from "typescript-result";
// Chaining multiple wrapped functions
function readConfig(filePath: string) {
return readFile(filePath)
.map((contents) => parseJSON(contents))
.map((json) => parseConfig(json));
}
// Using mapCatching to inline a throwing operation
function readConfigInline(filePath: string) {
return readFile(filePath)
.mapCatching(
(contents) => JSON.parse(contents),
() => new ParseError(`Unable to parse JSON`)
)
.map((json) => parseConfig(json));
}The library supports two primary patterns for interacting with Result and AsyncResult instances:
.map() to transform values. This is best for simple, single-line transformations and keeps code compact. It allows for centralized error handling at the end of the chain.yield* to write code that looks like normal sequential operations. This is best for complex control flows involving loops, conditionals, or deeply nested transformations.Decision Guide:
// Chaining style (Functional)
const result = someOperation()
.map((value) => anotherOperation(value))
.map((value) => yetAnotherOperation(value));
// Generator style (Imperative)
function* getValues() {
const a = yield* operationA();
const b = yield* operationB(a);
return b;
}
const result = Result.gen(getValues());