](https://www.npmjs.com/package/@anyhow/std)
A batteries-included TypeScript utility toolkit featuring type-safe error handling, optional values, branded types, runtime guards, schema validation, async primitives, iterators, formatting, string utilities, math, random, and caching.
bun add @anyhow/std
bun add @anyhow/cliA class-based discriminated union for type-safe error handling. Methods chain like Rust — no standalone functions needed.
import { ok, err, Result, Pipeline, pipeline } from "@anyhow/std/result";
import type { Result as R } from "@anyhow/std/result";
function divide(a: number, b: number): R<number, string> {
if (b === 0) return err("division by zero");
return ok(a / b);
}
// ── Method chaining ──
divide(10, 2)
.map((v) => v * 2) // Ok(10)
.andThen((v) => divide(v, 5)) // Ok(1)
.tap(console.log) // side effect, passes through
.unwrapOr(0); // 1
// ── Pattern matching ──
const label = divide(10, 0).match(
(v) => `Got ${v}`,
(e) => `Error: ${e}`,
); // "Error: division by zero"
// ── Fallback chains ──
divide(10, 0)
.or(ok(0)) // fall back to 0
.orElse((e) => ok(`recovered: ${e}`)); // lazy fallback
// ── Transform the error ──
divide(10, 0)
.mapErr((e) => new Error(e)) // wrap the error
.expect("should have a value"); // throws with custom message
// ── Static combinators ──
Result.from(() => JSON.parse(text)); // wrap throwy sync
Result.fromAsync(() => fetch("/api")); // wrap throwy async
Result.fromNullable(env.PORT, new Error("…")); // nullable → Result
Result.json('{"name":"Alice"}'); // safe JSON parse → Result
Result.jsonStringify({ name: "Alice" }, 2); // safe stringify
Result.parseInt("42"); // safe parseInt → Result
Result.parseFloat("3.14"); // safe parseFloat → Result
Result.decodeURIComponent("hello%20world"); // safe URI decode
Result.all([ok(1), ok(2), ok(3)]); // Ok([1, 2, 3])
Result.partition([ok(1), err("a"), ok(2)]); // { ok: [1,2], err: ["a"] }
Result.any([err("a"), ok(42)]); // Ok(42)
// ── Pipeline (reusable, observable stages) ──
const orderPipe = pipeline<RawOrder>()
.pipe("parse", parseOrder)
.pipe("validate", validateOrder)
.pipeWithRecovery("save", saveOrder, (err, input) => ok(queueForRetry(input)));
orderPipe.run(rawOrder);
orderPipe.describe(); // ["parse", "validate", "save"]
// ── Stepper (state machine for wizards / checkouts) ──
import { Stepper } from "@anyhow/std/result";
const checkout = new Stepper<"cart" | "ship" | "pay", CartData, string>()
.step("cart", validateCart)
.step("ship", validateShipping)
.after("cart", ["ship"])
.after("ship", ["pay", "cart"]);
checkout.run("cart", initialData, ["ship", "pay"]);
checkout.nextSteps("cart"); // ["ship"]A class-based discriminated union for optional values. Some<T> carries a value;
None represents absence. Methods chain like Rust.
import { some, none, Option } from "@anyhow/std/option";
import type { Option as O } from "@anyhow/std/option";
const opt = some(42);
// ── Method chaining ──
some(42)
.map((v) => v * 2) // Some(84)
.filter((v) => v > 0) // Some(84) (would be None if ≤ 0)
.andThen((v) => some(v + 1)) // Some(85)
.unwrapOr(0); // 85
// ── Fallback chains ──
none()
.or(some(10)) // Some(10)
.orElse(() => some(20)) // lazy fallback
.match(
(v) => `Got ${v}`,
() => "Nothing",
); // "Got 10"
// ── Type-narrowing guards ──
const o: O<number> = some(42);
if (o.isSome()) {
o.value; // number (narrowed)
}
if (o.isNone()) {
// o is None here
}
// ── Zip ──
some("a").zip(some(1)); // Some(["a", 1])
some(2).zipWith(some(3), (a, b) => a * b); // Some(6)
// ── Flatten ──
some(some(5)).flatten(); // Some(5)
some(none()).flatten(); // None
// ── Static combinators ──
Option.fromNullable(maybeNull); // Some(value) or None
Option.okOr(some(5), "missing"); // Ok(5)
Option.transpose(some(ok(5))); // Ok(Some(5))Runtime type guards and assertions for validating unknown data at the boundary.
import {
isString,
isNumber,
isBoolean,
isObject,
isDefined,
isArray,
isFunction,
isDate,
isRegExp,
isError,
isSymbol,
isBigInt,
isPromise,
isMap,
isSet,
isNull,
isUndefined,
isNotNullish,
isTruthy,
isFalsy,
isPrimitive,
isIterable,
isAsyncIterable,
hasProperty,
isArrayOf,
assert,
assertDefined,
assertNever,
invariant,
} from "@anyhow/std/guard";
// Type guards
const data: unknown = { name: "Alice", scores: [1, 2, 3] };
if (isObject(data) && hasProperty(data, "name", isString)) {
data.name; // string
}
if (isObject(data) && hasProperty(data, "scores", (v): v is number[] => isArrayOf(v, isNumber))) {
data.scores; // number[]
}
// Assertions (throw on failure)
assertDefined(process.env.API_KEY, "API_KEY");
// Exhaustiveness check
type Shape = { kind: "circle"; radius: number } | { kind: "square"; side: number };
function area(s: Shape): number {
switch (s.kind) {
case "circle":
return Math.PI * s.radius ** 2;
case "square":
return s.side ** 2;
default:
return assertNever(s, `Unhandled shape kind: ${(s as any).kind}`);
}
}
// Invariant checks (strippable for production)
invariant(limit > 0, "limit must be positive");Compile-time nominal (branded) types for type-safe domain modeling. Zero runtime cost — brands are erased at compile time.
import { type Brand, type Unbrand, type BrandOf, brand } from "@anyhow/std/brand";
// Create distinct types from the same base type
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
const uid: UserId = brand("usr_1");
const oid: OrderId = brand("ord_1");
// uid === oid; // Type error — UserId !== OrderId
// Works with any base type
type Meters = Brand<number, "Meters">;
type Feet = Brand<number, "Feet">;
const m: Meters = brand(100);
const f: Feet = brand(328);
// Extract the underlying type
type Raw = Unbrand<UserId>; // string
// Extract the brand
type B = BrandOf<UserId>; // "UserId"Branded types are used as a primitive by @anyhow/std/schema via s.brand().
Primitives for timing, retries, concurrency, and memoization.
import {
sleep,
debounce,
throttle,
retry,
concurrent,
Semaphore,
memoizeAsync,
} from "@anyhow/std/async";
// Debounce rapid calls (fire immediately on first call, then debounce)
const onChange = debounce((query: string) => search(query), 300, { leading: true });
// Throttle to at most one call per interval (fire trailing call at end)
const onScroll = throttle(() => updatePosition(), 100, { trailing: true });
// Retry with exponential backoff, returns a Result
const result = await retry(() => fetch("/api").then((r) => r.json()), {
attempts: 5,
backoff: 200, // starts at 200ms, then 400ms, 800ms, 1600ms
shouldRetry: (e) => e instanceof TypeError || (e as any).status === 429, // only retry certain errors
onRetry: (e, i) => console.warn(`Attempt ${i} failed:`, e),
signal: AbortSignal.timeout(10_000), // give up after 10s total
});
if (result.ok) console.log(result.value);
// Run promises with a concurrency limit
const results = await concurrent(
[fn1, fn2, fn3, fn4, fn5],
2, // only 2 at a time
{ ordered: false }, // results in completion order
);
// Semaphore — flexible concurrency limit for any async work
const api = new Semaphore(5);
const pages = await Promise.all(
urls.map((url) => api.acquire(() => fetch(url).then((r) => r.json()))),
);
// Memoize an expensive async function with a custom key resolver
const memoized = memoizeAsync(fetchUser, {
maxSize: 100,
ttlMs: 60_000,
resolver: (userId: string) => userId, // use userId as key directly
});Case conversion, slugification, string templating, and indent stripping.
import {
camelCase,
pascalCase,
snakeCase,
kebabCase,
slugify,
stripIndent,
template,
escapeHtml,
unescapeHtml,
escapeRegExp,
lines,
words,
capitalize,
randomString,
decapitalize,
reverse,
padStart,
padEnd,
wrap,
byteLength,
} from "@anyhow/std/string";
// Case conversion
camelCase("hello-world"); // "helloWorld"
pascalCase("hello-world"); // "HelloWorld"
snakeCase("helloWorld"); // "hello_world"
kebabCase("helloWorld"); // "hello-world"
// Slugify for URLs
slugify("Hello World!"); // "hello-world"
// Strip common leading whitespace
stripIndent(`
hello
world
`); // "hello\nworld"
// Simple template substitution (returns Result)
const r = template("Hello {{name}}!", { name: "Alice" });
if (r.ok) console.log(r.value); // "Hello Alice!"
// HTML escaping
escapeHtml("<div>hello & goodbye</div>"); // "<div>hello & goodbye</div>"
unescapeHtml("<div>hello&goodbye</div>"); // "<div>hello&goodbye</div>"
// Escape for regex
escapeRegExp("1 + 1 = 2?"); // "1 \\+ 1 = 2\\?"
// Split into lines or words
lines("a\nb\nc"); // ["a", "b", "c"]
words("hello world"); // ["hello", "world"]
// Case manipulation
capitalize("hello"); // "Hello"
decapitalize("Hello"); // "hello"
// Padding and wrapping
padStart("42", 5, "0"); // "00042"
padEnd("42", 5, "0"); // "42000"
wrap("hello world", 5); // ["hello", "world"]
// Other utilities
reverse("hello"); // "olleh"
byteLength("hello"); // 5
randomString(8); // "a3f1b2c0"Human-readable formatting for strings, numbers, dates, and units.
import {
truncate,
pluralize,
filesize,
duration,
durationMs,
currency,
number,
date,
relativeTime,
relativeTimeFromNow,
list,
ordinal,
compact,
percentage,
durationHuman,
scientific,
engineering,
} from "@anyhow/std/fmt";
// Strings
truncate("hello world", 8); // "hello w…"
truncate("hello world", 8, { position: "middle" }); // "hel…rld"
pluralize(1, "cat"); // "1 cat"
pluralize(3, "cat"); // "3 cats"
// Units
filesize(1_500_000); // "1.5 MB"
filesize(1_500_000, { binary: true }); // "1.4 MiB"
duration(3_661_000); // "1h 1m 1s"
duration(3_661_000, { maxParts: 2 }); // "1h 1m"
// Intl wrappers
currency(9.99, "USD"); // "$9.99"
number(1_234_567.89); // "1,234,567.89"
date(new Date(), "full"); // "Monday, January 1, 2024"
relativeTime(-3, "day"); // "3 days ago"
relativeTimeFromNow(Date.now() - 60_000); // "1 minute ago"
list(["Alice", "Bob", "Carol"]); // "Alice, Bob, and Carol"
// More formatters
ordinal(3); // "3rd"
compact(1_234_567); // "1.2M"
percentage(0.857, 2); // "85.70%"
percentage(0.5); // "50%"
durationMs(450); // "450ms"
durationHuman(3_661_000); // "1 hour 1 minute 1 second"
scientific(1_234_567); // "1.234567e6"
engineering(1_234_567); // "1.234567M"Lazy iterator combinators over arrays.
import {
map,
filter,
flatMap,
take,
skip,
takeWhile,
skipWhile,
scan,
cycle,
repeat,
intersperse,
interleave,
flatten,
windows,
enumerate,
unique,
zip,
chunk,
first,
last,
count,
find,
some,
every,
reduce,
forEach,
groupBy,
sortBy,
partition,
minBy,
maxBy,
} from "@anyhow/std/iter";
map([1, 2, 3], (n) => n * 2); // [2, 4, 6]
filter([1, 2, 3, 4], (n) => n % 2 === 0); // [2, 4]
take([1, 2, 3, 4, 5], 3); // [1, 2, 3]
chunk([1, 2, 3, 4, 5], 2); // [[1, 2], [3, 4], [5]]
zip(["a", "b"], [1, 2]); // [["a", 1], ["b", 2]]
unique([1, 2, 2, 3, 3, 3]); // [1, 2, 3]
groupBy([1, 2, 3, 4, 5], (n) => (n % 2 === 0 ? "even" : "odd"));
// Map { "odd" => [1, 3, 5], "even" => [2, 4] }
// Conditional iteration
takeWhile([1, 2, 3, 4, 5], (n) => n < 4); // [1, 2, 3]
skipWhile([1, 2, 3, 4, 5], (n) => n < 3); // [3, 4, 5]
// Accumulating scan
scan([1, 2, 3], (acc, n) => acc + n, 0); // [1, 3, 6]
// Cycling
cycle(["a", "b"], 5); // ["a", "b", "a", "b", "a"]
// Repeating a value
repeat(42, 3); // [42, 42, 42]
// Interleaving
intersperse([1, 2, 3], 0); // [1, 0, 2, 0, 3]
interleave([1, 3], [2, 4]); // [1, 2, 3, 4]
// Flattening and windows
flatten([
[1, 2],
[3, 4],
]); // [1, 2, 3, 4]
windows([1, 2, 3, 4], 2); // [[1, 2], [2, 3], [3, 4]]
// Sorting
sortBy([3, 1, 4, 1, 5]); // [1, 1, 3, 4, 5]
sortBy(["cat", "elephant", "dog"], (s) => s.length); // ["cat", "dog", "elephant"]
// Partition and extremum
partition([1, 2, 3, 4], (n) => n % 2 === 0);
// { matching: [2, 4], nonMatching: [1, 3] }
minBy(["cat", "elephant", "dog"], (s) => s.length); // "cat"
maxBy(["cat", "elephant", "dog"], (s) => s.length); // "elephant"Interpolation, statistics, and numeric utilities.
import {
clamp,
lerp,
normalize,
mapRange,
roundTo,
degToRad,
radToDeg,
sum,
average,
median,
mode,
variance,
stddev,
min,
max,
product,
isEven,
isOdd,
isInteger,
isFloat,
sign,
inRange,
gcd,
lcm,
isPrime,
factorial,
fibonacci,
isPowerOfTwo,
} from "@anyhow/std/math";
// Interpolation & scaling
clamp(150, 0, 100); // 100
lerp(0, 100, 0.5); // 50
normalize(50, 0, 100); // 0.5
mapRange(50, 0, 100, 0, 1); // 0.5
roundTo(3.14159, 2); // 3.14
degToRad(180); // 3.14159…
radToDeg(Math.PI); // 180
// Statistics
sum([1, 2, 3, 4, 5]); // 15
average([1, 2, 3, 4, 5]); // 3
median([1, 5, 2, 4, 3]); // 3
mode([1, 2, 2, 3]); // 2
variance([1, 2, 3, 4, 5]); // 2.5
stddev([1, 2, 3, 4, 5]); // 1.581…
min([3, 1, 4, 1, 5]); // 1
max([3, 1, 4, 1, 5]); // 5
product([2, 3, 4]); // 24
// Number predicates
isEven(4); // true
isOdd(3); // true
isInteger(5.0); // true
isFloat(3.14); // true
sign(-5); // -1
inRange(5, 0, 10); // true
inRange(5, 0, 10, "()"); // true (exclusive bounds)
// Number theory
gcd(12, 8); // 4
lcm(4, 6); // 12
isPrime(17); // true
factorial(5); // 120
fibonacci(7); // 13
isPowerOfTwo(64); // trueSeeded PRNG (Mulberry32) with shuffle, pick, weighted choice, and a drop-in auto-seeded singleton.
import { random, createRandom } from "@anyhow/std/random";
// Drop-in use (auto-seeded)
random.int(1, 6); // 4
random.float(0, 1); // 0.573…
random.bool(); // true
random.pick(["a", "b", "c"]); // "b"
random.randomHex(4); // "a3f1b2c0"
random.randomColor(); // "#a3f1b2"
random.exponential(); // 0.573…
random.shuffle([1, 2, 3]); // [3, 1, 2]
random.weighted(["a", "b"], [0.9, 0.1]); // "a" most of the time
random.gaussian(100, 15); // normally-distributed (mean 100, stddev 15)
random.uuid(); // "a3f1b2c0-1234-4abc-9def-0123456789ab"
random.sample([1, 2, 3, 4, 5], 3); // [3, 1, 5]
// Deterministic (same seed = same sequence)
const rng = createRandom(42);
rng.int(1, 10); // always the same for seed 42An LRU cache with optional TTL, and a sync memoization helper.
import { LRU, memoizeSync } from "@anyhow/std/cache";
const cache = new LRU<string, User>(256, 60_000); // max 256 entries, 1 min TTL
cache.set("user:42", user);
const user = cache.get("user:42"); // User | undefined
// Auto-compute on miss
const user = cache.getOrSet("user:42", () => fetchUser("42"));
// Iterate over entries (oldest first, skips expired)
for (const [key, value] of cache) {
console.log(key, value);
}
console.log([...cache.keys()]); // ["user:42", ...]
console.log([...cache.values()]); // [User, ...]
// Memoize a sync function
const fib = memoizeSync(
(n: number): number => {
if (n <= 1) return n;
return fib(n - 1) + fib(n - 2);
},
{ maxSize: 1000 },
);Date arithmetic, comparison, boundaries, and queries. Zero-dependency date math
that fills the gap between new Date() and Intl formatting (which lives in @anyhow/std/fmt).
import {
addDays,
subDays,
addMonths,
addYears,
differenceInDays,
differenceInMonths,
isBefore,
isAfter,
isSameDay,
isToday,
isPast,
isFuture,
startOfDay,
endOfDay,
startOfWeek,
startOfMonth,
daysInMonth,
isLeapYear,
getQuarter,
dateRange,
clampDate,
minDate,
maxDate,
fromUnix,
toUnix,
} from "@anyhow/std/date";
// Arithmetic — always returns a new Date
const nextWeek = addDays(new Date(), 7);
const lastMonth = subMonths(new Date(), 1);
// Feb 29 + 1 year correctly clamps to Feb 28 on non-leap years
const nextBirthday = addYears(new Date("2024-02-29"), 1); // 2025-02-28
// Differences
const age = differenceInYears(new Date(), new Date("1990-05-15"));
const daysUntil = differenceInDays(new Date("2025-01-01"), new Date());
// Comparisons
isBefore(new Date("2024-01-01"), new Date("2024-06-15")); // true
isToday(someDate);
isPast(deadline);
isWeekend(new Date("2024-01-06")); // true (Saturday)
isLeapYear(new Date("2024-01-01")); // true
// Boundaries
startOfDay(new Date()); // today at 00:00:00.000
endOfMonth(new Date("2024-03-15")); // 2024-03-31T23:59:59.999
startOfWeek(new Date("2024-01-03")); // Monday Jan 1 (ISO week)
// Queries
daysInMonth(new Date("2024-02-01")); // 29 (leap year)
getQuarter(new Date("2024-07-01")); // 3
dayOfYear(new Date("2024-12-31")); // 366
// Ranges
for (const d of dateRange(new Date("2024-01-01"), new Date("2024-01-07"))) {
console.log(d); // Jan 1, Jan 2, ..., Jan 7
}
// Clamping & min/max
const safe = clampDate(userDate, new Date("2024-01-01"), new Date("2024-12-31"));
const earliest = minDate([d1, d2, d3]);
// Unix timestamps
const ts = toUnix(new Date()); // seconds
const d = fromUnix(1704067200); // DateHex, Base64, and UTF-8 encoding/decoding. Zero dependencies, works everywhere
TextEncoder/TextDecoder are available (browser, Node, Bun, Deno).
import {
toHex,
fromHex,
toBase64,
fromBase64,
toBase64Url,
fromBase64Url,
toUTF8,
fromUTF8,
} from "@anyhow/std/bytes";
// Hex
toHex(new Uint8Array([0, 255, 16])); // "00ff10"
fromHex("00ff10"); // Uint8Array [0, 255, 16]
fromHex("0x00ff"); // 0x prefix is stripped
// Standard Base64 (with +/ and = padding)
toBase64(toUTF8("hello")); // "aGVsbG8="
fromBase64("aGVsbG8="); // Uint8Array [104, 101, ...]
// URL-safe Base64 (with -_ and no padding) — safe in URLs, JWTs, etc.
toBase64Url(new Uint8Array([255])); // "_w" (vs "/w==" in standard)
toBase64Url(crypto.getRandomValues(new Uint8Array(32))); // "dGhpcyBpc..."
// UTF-8
const bytes = toUTF8("Hello, 世界!"); // Uint8Array
fromUTF8(bytes); // "Hello, 世界!"
// All to* functions accept Uint8Array, ArrayBuffer, or ArrayBufferView
const hash = await crypto.subtle.digest("SHA-256", toUTF8("data"));
toHex(hash); // works with ArrayBuffer directly
// fromHex / fromBase64 / fromBase64Url throw on invalid input
fromHex("xyz"); // throwsANSI styling, word wrapping, columns, progress bars, cursor controls, and
hyperlink helpers for terminal output. Used by @anyhow/cli internally.
import {
style,
stripAnsi,
supportsColor,
wordWrap,
columns,
progress,
clearScreen,
clearLine,
cursorTo,
cursorHide,
cursorShow,
link,
Spinner,
SPINNER_FRAMES,
} from "@anyhow/std/term";
// Chainable style builder
style.red("error"); // "\x1b[31merror\x1b[39m"
style.bold.red("critical"); // "\x1b[1;31mcritical\x1b[22;39m"
style.bgBlue.white("info"); // blue background, white text
style.rgb(255, 128, 0)("custom"); // 24-bit true color
style.hex("#ff8800")("custom"); // hex-based true color
// Save reusable styles
const err = style.bold.red;
const ok = style.green;
// Strip ANSI codes
stripAnsi("\x1b[31merror\x1b[39m"); // "error"
// Color support detection (respects NO_COLOR, FORCE_COLOR, TTY, CI)
if (supportsColor()) console.log(style.green("✓"));
// ANSI-aware word wrapping (preserves colors across line breaks)
wordWrap(style.red("long message"), 20);
wordWrap(style.red("long message"), 20, { indent: 2 });
// Columns like `ls` output
console.log(columns(["index.ts", "README.md", "pkg.json"], 40));
// Stateless progress bar
for (let i = 0; i <= 100; i++) {
process.stdout.write(`\r${progress(i / 100, 30)}`);
await sleep(50);
}
progress(0.5, 20, { left: "Loading", right: "50/100", style: "dot" });
// Cursor controls
process.stdout.write(clearScreen());
process.stdout.write(clearLine());
// OSC 8 hyperlinks (clickable in modern terminals)
console.log(link("View docs", "https://example.com"));
// Spinner with start/stop or run pattern
const spinner = new Spinner("Installing dependencies...");
spinner.start();
await doWork();
spinner.stop("✓ Installed");
// Or use run() to auto-stop (even on error)
const result = await spinner.run(() => fetchData());
// Custom frames and interval
const dots = new Spinner({ text: "Thinking", frames: SPINNER_FRAMES.dots, interval: 80 });SemVer parsing, comparison, range matching (caret, tilde, wildcards, operators), and version bumping.
import { semver } from "@anyhow/std/semver";
const v = semver("1.2.3");
v.major; // 1
v.lt("2.0.0"); // true (accepts strings)
v.satisfies("^1.0.0"); // true
const next = v.bump("minor"); // new SemVerObj
next.toString(); // "1.3.0"
next.bump("patch").toString(); // "1.3.1" — chains
// Static helpers
semver.valid("1.2.3"); // true
semver.coerce("v1.2"); // SemVerObj("1.2.0")
semver.sort(["2.0.0", "1.0.0"]); // ["1.0.0", "2.0.0"]
semver.max(["1.0.0", "2.0.0"]); // "2.0.0"
semver.diff("1.0.0", "1.0.1"); // "patch"
// Low-level functional API still available
import { parse, compare, satisfies, bump } from "@anyhow/std/semver";Safe filesystem operations that return {@link Result} instead of throwing. All functions auto-create parent directories as needed and never leave you to guess which errors Node might throw.
import {
readText,
readJson,
writeText,
writeJson,
ensureDir,
remove,
exists,
tmpDir,
glob,
walk,
} from "@anyhow/std/fs";
// Read and write safely
const text = await readText("./file.txt");
if (text.ok) console.log(text.value);
const config = await readJson<CliConfig>("./config.json");
if (!config.ok) {
console.error("Bad config:", config.error);
process.exit(1);
}
await writeJson("./out/data.json", { name: "Alice" }, 2);
// Directory helpers
await ensureDir("./a/b/c"); // mkdir -p
await remove("./tmp"); // rm -rf
if (await exists("./file.txt")) {
} // fs.existsSync, but async
// Create a temp directory (caller cleans up)
const tmp = await tmpDir("build-");
if (tmp.ok) {
await writeText(`${tmp.value}/out.txt`, "...");
await remove(tmp.value);
}
// Glob matching
const files = await glob("src/**/*.ts");
if (files.ok) {
for (const file of files.value) {
console.log(file); // "src/a.ts", "src/lib/b.ts", ...
}
}
// Lazy directory walking
for await (const entry of walk("./src")) {
console.log(entry.path, entry.isDir ? "(dir)" : "(file)");
}Runtime schema validation that returns Result<T, ValidationError>. Composes with @anyhow/std/result and @anyhow/std/safe.
import { s, type Infer } from "@anyhow/std/schema";
// Primitives
s.string();
s.number();
s.boolean();
s.literal("hello");
s.enum(["a", "b", "c"]);
// Composites
const User = s.object({
name: s.string(),
age: s.number(),
tags: s.array(s.string()).optional().default([]),
});
type User = Infer<typeof User>; // { name: string; age: number; tags: string[] }
// Parse
const result = User.parse({ name: "Alice", age: 30 });
// { ok: true, value: { name: "Alice", age: 30, tags: [] } }
User.parse({ name: 42 });
// { ok: false, error: { path: "name", expected: "string", ... } }
// Modifiers
s.string().optional(); // string | undefined
s.string().nullable(); // string | null
s.string().default(""); // fills undefined with ""
// More shapes
s.array(s.number()); // number[]
s.tuple([s.string(), s.number()]); // [string, number]
s.union([s.string(), s.number()]); // string | number
s.record(s.number()); // Record<string, number>
s.date(); // Date
s.lazy(() => s.number()); // recursive schemas
s.coerce(s.string(), (v) => String(v)); // coerce values
s.brand(s.number(), "USD"); // branded (nominal) types
s.any(); // any value (no validation)
s.undefined(); // undefined only
s.null(); // null only
s.instanceof(Date); // instanceof check
// Object methods
const Admin = User.extend({ role: s.string() }); // add fields
const Public = User.omit(["age"]); // remove fields
const Subset = User.pick(["name"]); // keep only these fieldsDeclarative CLI framework — define commands as plain objects with full type inference.
bun add @anyhow/cliimport { defineCommand, defineCli } from "@anyhow/cli";
const deploy = defineCommand({
name: "deploy",
description: "Deploy to environment",
arguments: { env: { type: "string", required: true, description: "Target environment" } },
options: { force: { type: "boolean", short: "f", description: "Skip confirmation" } },
async action({ args, options }) {
// args.env is string, options.force is boolean
console.log(`Deploying to ${args.env}`);
},
});
const cli = defineCli({
name: "mycli",
description: "A sample CLI",
commands: [deploy],
});
await cli.run(process.argv.slice(2));# Install dependencies
bun install
# Build all packages
bun run build
# Run tests
bun test
# Lint & format
bun run lint
bun run format
# Check everything
bun run checkMIT