Auto-Pilot / 全自动
更新于 a month ago

better-result

Ddmmulroy
0.7k
dmmulroy/better-result
72
Agent 评分

💡 摘要

中文总结。

🎯 适合人群

用户画像1用户画像2用户画像3

🤖 AI 吐槽:看起来很能打,但别让配置把人劝退。

安全分析低风险

风险:Low。建议检查:是否执行 shell/命令行指令;是否发起外网请求(SSRF/数据外发);文件读写范围与路径穿越风险;依赖锁定与供应链风险。以最小权限运行,并在生产环境启用前审计代码与依赖。

better-result

Lightweight Result type for TypeScript with generator-based composition.

Install

New to better-result?

npx better-result init

Upgrading from v1?

npx better-result migrate

Quick Start

import { Result } from "better-result"; // Wrap throwing functions const parsed = Result.try(() => JSON.parse(input)); // Check and use if (Result.isOk(parsed)) { console.log(parsed.value); } else { console.error(parsed.error); } // Or use pattern matching const message = parsed.match({ ok: (data) => `Got: ${data.name}`, err: (e) => `Failed: ${e.message}`, });

Contents

Creating Results

// Success const ok = Result.ok(42); // Error const err = Result.err(new Error("failed")); // From throwing function const result = Result.try(() => riskyOperation()); // From promise const result = await Result.tryPromise(() => fetch(url)); // With custom error handling const result = Result.try({ try: () => JSON.parse(input), catch: (e) => new ParseError(e), });

Transforming Results

const result = Result.ok(2) .map((x) => x * 2) // Ok(4) .andThen( ( x, // Chain Result-returning functions ) => (x > 0 ? Result.ok(x) : Result.err("negative")), ); // Standalone functions (data-first or data-last) Result.map(result, (x) => x + 1); Result.map((x) => x + 1)(result); // Pipeable

Handling Errors

// Transform error type const result = fetchUser(id).mapError( (e) => new AppError(`Failed to fetch user: ${e.message}`), ); // Recover from specific errors const result = fetchUser(id).match({ ok: (user) => Result.ok(user), err: (e) => e._tag === "NotFoundError" ? Result.ok(defaultUser) : Result.err(e), });

Extracting Values

// Unwrap (throws on Err) const value = result.unwrap(); const value = result.unwrap("custom error message"); // With fallback const value = result.unwrapOr(defaultValue); // Pattern match const value = result.match({ ok: (v) => v, err: (e) => fallback, });

Generator Composition

Chain multiple Results without nested callbacks or early returns:

const result = Result.gen(function* () { const a = yield* parseNumber(inputA); // Unwraps or short-circuits const b = yield* parseNumber(inputB); const c = yield* divide(a, b); return Result.ok(c); }); // Result<number, ParseError | DivisionError>

Async version with Result.await:

const result = await Result.gen(async function* () { const user = yield* Result.await(fetchUser(id)); const posts = yield* Result.await(fetchPosts(user.id)); return Result.ok({ user, posts }); });

Errors from all yielded Results are automatically collected into the final error union type.

Normalizing Error Types

Use mapError on the output of Result.gen() to unify multiple error types into a single type:

class ParseError extends TaggedError("ParseError")<{ message: string }>() {} class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {} class AppError extends TaggedError("AppError")<{ source: string; message: string }>() {} const result = Result.gen(function* () { const parsed = yield* parseInput(input); // Err: ParseError const valid = yield* validate(parsed); // Err: ValidationError return Result.ok(valid); }).mapError((e): AppError => new AppError({ source: e._tag, message: e.message })); // Result<ValidatedData, AppError> - error union normalized to single type

Retry Support

const result = await Result.tryPromise(() => fetch(url), { retry: { times: 3, delayMs: 100, backoff: "exponential", // or "linear" | "constant" }, });

Conditional Retry

Retry only for specific error types using shouldRetry:

class NetworkError extends TaggedError("NetworkError")<{ message: string }>() {} class ValidationError extends TaggedError("ValidationError")<{ message: string }>() {} const result = await Result.tryPromise( { try: () => fetchData(url), catch: (e) => e instanceof TypeError // Network failures often throw TypeError ? new NetworkError({ message: (e as Error).message }) : new ValidationError({ message: String(e) }), }, { retry: { times: 3, delayMs: 100, backoff: "exponential", shouldRetry: (e) => e._tag === "NetworkError", // Only retry network errors }, }, );

Async Retry Decisions

For retry decisions that require async operations (rate limits, feature flags, etc.), enrich the error in the catch handler instead of making shouldRetry async:

class ApiError extends TaggedError("ApiError")<{ message: string; rateLimited: boolean; }>() {} const result = await Result.tryPromise( { try: () => callApi(url), catch: async (e) => { // Fetch async state in catch handler const retryAfter = await redis.get(`ratelimit:${userId}`); return new ApiError({ message: (e as Error).message, rateLimited: retryAfter !== null, }); }, }, { retry: { times: 3, delayMs: 100, backoff: "exponential", shouldRetry: (e) => !e.rateLimited, // Sync predicate uses enriched error }, }, );

UnhandledException

When Result.try() or Result.tryPromise() catches an exception without a custom handler, the error type is UnhandledException:

import { Result, UnhandledException } from "better-result"; // Automatic — error type is UnhandledException const result = Result.try(() => JSON.parse(input)); // ^? Result<unknown, UnhandledException> // Custom handler — you control the error type const result = Result.try({ try: () => JSON.parse(input), catch: (e) => new ParseError(e), }); // ^? Result<unknown, ParseError> // Same for async await Result.tryPromise(() => fetch(url)); // ^? Promise<Result<Response, UnhandledException>>

Access the original exception via .cause:

if (Result.isError(result)) { const original = result.error.cause; if (original instanceof SyntaxError) { // Handle JSON parse error } }

Panic

Thrown (not returned) when user callbacks throw inside Result operations. Represents a defect in your code, not a domain error.

import { Panic } from "better-result"; // Callback throws → Panic Result.ok(1).map(() => { throw new Error("bug"); }); // throws Panic // Generator cleanup throws → Panic Result.gen(function* () { try { yield* Result.err("expected failure"); } finally { throw new Error("cleanup bug"); } }); // throws Panic // Catch handler throws → Panic Result.try({ try: () => riskyOp(), catch: () => { throw new Error("bug in handler"); }, }); // throws Panic

Why Panic? Err is for recoverable domain errors. Panic is for bugs — like Rust's panic!(). If your .map() callback throws, that's not an error to handle, it's a defect to fix. Returning Err would collapse type safety (Result<T, E> becomes Result<T, E | unknown>).

Panic properties:

| Property | Type | Description | | --------- | --------- | ----------------------------------- | | message | string | Describes where/what panicked | | cause | unknown | The exception that was thrown |

Panic also provides toJSON() for error reporting services (Sentry, etc.).

Tagged Errors

Build exhaustive error handling with discriminated unions:

import { TaggedError, matchError, matchErrorPartial } from "better-result"; // Factory API: TaggedError("Tag")<Props>() class NotFoundError extends TaggedError("NotFoundError")<{ id: string; message: string; }>() {} class ValidationError extends TaggedError("ValidationError")<{ field: string; message: string; }>() {} type AppError = NotFoundError | ValidationError; // Create errors with object args const err = new NotFoundError({ id: "123", message: "User not found" }); // Exhaustive matching matchError(error, { NotFoundError: (e) => `Missing: ${e.id}`, ValidationError: (e) => `Bad field: ${e.field}`, }); // Partial matching with fallback matchErrorPartial( error, { NotFoundError: (e) => `Missing: ${e.id}` }, (e) => `Unknown: ${e.message}`, ); // Type guards TaggedError.is(value); // any tagged error NotFoundError.is(value); // specific class

For errors with computed messages, add a custom constructor:

class NetworkError extends TaggedError("NetworkError")<{ url: string; status: number; message: string; }>() { constructor(args: { url: string; status: number }) { super({ ...args, message: `Request to ${args.url} failed: ${args.status}` }); } } new NetworkError({ url: "/api", status: 404 });

Serialization

Convert Results to plain objects for RPC, storage, or server actions:

import { Result, SerializedResult } from "better-result"; // Serialize to plain object const result = Result.ok(42); const serialized = Result.serialize(result); // { status: "ok", value: 42 } // Deserialize back to Result instance const deserialized = Result.deserialize<number, never>(serialized); // Ok(42) - can use .map(), .andThen(), etc. // Typed boundary for Next.js server actions async function createUser(data: FormData): Promise<SerializedResult<User, ValidationError>> { const result = await validateAndCreate(data); return Result.serialize(result); } // Client-side const serialized = await createUser(formData); const result = Result.deserialize<User, ValidationError>(serialized);

API Refer

五维分析
清晰度8/10
创新性6/10
实用性8/10
完整性7/10
可维护性7/10
优缺点分析

优点

  • 优点1
  • 优点2

缺点

  • 缺点1
  • 缺点2

相关技能

claude-domain-skills

B
toolAuto-Pilot / 全自动
72/ 100

“看起来很能打,但别让配置把人劝退。”

my-skills

B
toolAuto-Pilot / 全自动
72/ 100

“看起来很能打,但别让配置把人劝退。”

terraform-ibm-modules-skills

B
toolAuto-Pilot / 全自动
72/ 100

“看起来很能打,但别让配置把人劝退。”

免责声明:本内容来源于 GitHub 开源项目,仅供展示和评分分析使用。

版权归原作者所有 dmmulroy.