typescript-result

repository·master·Indexed 19 days ago

https://github.com/everweij/typescript-result

A 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.

Tokens
27.6K
Snippets
82
Records
100
Agent score
63%

What's inside typescript-result

  1. Work with Results using Generator functions

    master
    For developers who prefer an imperative coding style over functional chaining, the library supports generator functions. This allows you to write code that looks more like standard synchronous logic while maintaining the type safety and error handling benefits of the Result type. This is an optional feature inspired by patterns used in EffectTS.
  2. Use nesting to transform errors locally

    master

    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>
    );
  3. Use AsyncResult for seamless async chaining

    master

    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!");
      });
  4. Handle asynchronous operations with AsyncResult

    master

    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>
  5. Use polymorphic `map` to change Result types

    master

    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:

    • Returning a plain value: Keeps the same Result type.
    • Returning a Result.ok(...): Keeps the same Result type (due to automatic flattening).
    • Returning a Promise or using an async function: Converts the Result into an AsyncResult.
    • Returning a Generator (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>
  6. Use errors as values instead of exceptions

    master

    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:

    • Distinction of Errors: You can easily distinguish between expected domain errors (e.g., UserNotFound) and unexpected system failures.
    • Type Safety: TypeScript's type system tracks every possible failure scenario. Because the error is part of the return type, the compiler forces you to handle failure cases, catching potential bugs at compile time.
    • Transparency: The function signature explicitly communicates that an operation can fail, making the code more maintainable and readable.
  7. Understand the Result type concept

    master

    A Result type is a container (or "box") that represents the outcome of an operation. It can hold one of two states:

    1. Success (OK): Contains the successful value of type T.
    2. Failure (Err): Contains an error value of type 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>;
  8. Leverage type inference with Result.ok() and Result.error()

    master
    You can rely on TypeScript's type inference to reduce boilerplate. Instead of explicitly declaring complex generic types, you can simply return Result.ok(value) or Result.error(error) and let the compiler infer the success and failure types based on the context.
  9. Use exhaustive checks with `match()`

    master

    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);
    }
  10. Use `noImplicitReturn` for exhaustive checks in functions

    master

    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
      }
    });
  11. Chain Result operations with .map() and .mapCatching()

    master

    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));
    }
  12. Compare Chaining vs. Generator styles

    master

    The library supports two primary patterns for interacting with Result and AsyncResult instances:

    1. Chaining Style (Functional): Uses methods like .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.
    2. Generator Style (Imperative): Uses generator functions and 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:

    • Use Chaining if you are performing simple transformations.
    • Use Generators if you have complex logic, loops, or many nested steps.
    // 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());