Represent recoverable failure as typed data and plain absence as null.
@hraness/result provides a dependency-free Result<T, E> and Option<T> for
TypeScript without adding a framework or runtime policy.
Check result.ok to narrow the result to its success value or declared error
type.
Pin the GitHub dependency to a version tag. The current release is
v0.2.1.
{
"dependencies": {
"@hraness/result": "github:hraness/result#v0.2.1"
}
}bun installThe lockfile records the resolved commit. Upgrading the package requires an explicit tag change.
This example uses Option<string> for an input that can be absent and
Result<number, PortError> for validation that can fail.
import { err, ok, type Option, type Result } from "@hraness/result";
type PortError =
| { readonly kind: "missing" }
| { readonly kind: "invalid"; readonly input: string };
function parsePort(input: Option<string>): Result<number, PortError> {
if (input === null) return err({ kind: "missing" });
const port = Number(input);
return Number.isInteger(port) && port > 0 && port <= 65_535
? ok(port)
: err({ kind: "invalid", input });
}
const result = parsePort("3000");
if (result.ok) {
console.log(result.value); // 3000
} else {
console.error(result.error.kind);
}The constructors return ordinary objects:
parsePort("3000"); // { ok: true, value: 3000 }
parsePort(null); // { ok: false, error: { kind: "missing" } }
parsePort("auto"); // { ok: false, error: { kind: "invalid", input: "auto" } }No exception represents these expected outcomes. Result keeps the failure
type on the function signature, while Option<T> means only T | null.
Use a discriminated error union when callers must handle different recovery
paths. assertNever turns an omitted case into a type error and retains a
runtime backstop for values that bypass TypeScript.
import { assertNever } from "@hraness/result";
function describePortError(error: PortError): string {
switch (error.kind) {
case "missing":
return "Set PORT.";
case "invalid":
return `PORT must be an integer from 1 through 65535, received ${error.input}.`;
default:
return assertNever(error);
}
}
const parsed = parsePort("auto");
if (!parsed.ok) console.error(describePortError(parsed.error));Adding another PortError variant makes the default branch fail typechecking
until describePortError handles it.
The constructors keep the discriminant literal and infer their payloads:
const success = ok(42); // Ok<number>
const failure = err("stop"); // Err<string>An annotated function combines those variants into a Result<T, E>. Checking
result.ok, isOk(result), or isErr(result) narrows without a cast.
import { mapOk, unwrapOr } from "@hraness/result";
const parsed = parsePort("3000");
const label = mapOk(parsed, (port) => `:${port}`); // Result<string, PortError>
const port = unwrapOr(parsed, 8080); // numbermapOk transforms only the success value and preserves the failure type.
unwrapOr returns the success value or a caller-supplied fallback of the same
success type.
Result does not catch exceptions for you. In particular, mapOk calls your transform directly on success; an exception from that callback propagates to the caller. On failure, it returns the failed result without calling the transform.
Use err(...) for the recoverable outcomes you declare in your function's return type. If you wrap a throwing API, catch its exception in your own adapter and decide which errors are recoverable. unwrapOr supplies a fallback for an Err; it does not recover from an exception thrown before the result exists.
The readonly fields are a TypeScript constraint, not a runtime freeze. The constructors return ordinary objects, so do not rely on them to freeze a payload or validate foreign input.
| Export | Type or signature | Contract |
|---|---|---|
Result<T, E = Error> |
Ok<T> | Err<E> |
Recoverable success or failure. |
Ok<T> |
{ readonly ok: true; readonly value: T } |
Successful variant. |
Err<E> |
{ readonly ok: false; readonly error: E } |
Failed variant. |
Option<T> |
T | null |
Plain absence. It does not include undefined. |
ok(value) |
T -> Ok<T> |
Construct a success. |
err(error) |
E -> Err<E> |
Construct a failure. |
isOk(result) |
Type guard | Narrow a Result to Ok<T>. |
isErr(result) |
Type guard | Narrow a Result to Err<E>. |
mapOk(result, transform) |
Result<T, E> -> Result<U, E> |
Transform success and preserve failure. |
unwrapOr(result, fallback) |
Result<T, E> -> T |
Read success or use a fallback. |
isRecord(value) |
unknown -> Record<PropertyKey, unknown> |
Exclude null, arrays, functions, and primitives before reading foreign fields. |
assertNever(value) |
never -> never |
Enforce exhaustive handling and throw if an impossible value reaches runtime. |
The package does not provide matching syntax, asynchronous combinators, validation accumulation, or application-specific error classes.
| Concern | Contract |
|---|---|
| Runtime graph | Zero runtime dependencies. |
| Module format | ESM only. There is no CommonJS require export. |
| Runtime artifact | GitHub installs execute the included dist/index.js. |
| Type artifact | TypeScript reads src/index.ts through the package export map. |
| Bun and Node.js | Import the package as ESM. |
| TypeScript | Supports Bundler and NodeNext module resolution. |
The GitHub dependency includes its JavaScript artifact, so you do not need to build the package before importing it.
| Situation | Use |
|---|---|
| A caller can recover from a declared failure | Result<T, E>. The failure remains visible in the return type. |
| A value can be absent without an error | Option<T>. Use null as the one absence state. |
| An invariant is impossible in a well-typed program | An assertion or thrown error. assertNever covers exhaustive unions. |
A local function already returns T | null |
Keep that shape. Option<T> adds a shared name, not new runtime behavior. |
| A product needs async pipelines, validation accumulation, matching syntax, or a large combinator set | Use neverthrow for ResultAsync and method chaining, true-myth for Result and Maybe classes with methods, or Effect for typed errors with concurrency and dependency injection. This package stops at the primitives above. |
Compared with an object that independently makes value and error optional,
the ok discriminant rules out both-present and neither-present states. Compared
with thrown recoverable errors, Result<T, E> makes the failure part of the
caller's typechecking path.
Browse GitHub Releases for version history. The contributor guide explains how to prepare a release.
Read CONTRIBUTING.md before opening a pull request.
bun install --frozen-lockfile --ignore-scripts
bun run checkReport suspected vulnerabilities privately as described in SECURITY.md.
MIT
Maintained by Hraness.