@bloodyowl/boxed

repository·main·Indexed 20 days ago

https://github.com/bloodyowl/boxed

A library of essential building blocks, including types and functions, designed for writing functional, safe, and immutable TypeScript code. It provides core types such as Option, Result, Future, AsyncData, and Lazy, along with utilities for Deferred, Dict, and Array. The library focuses on immutability, chaining APIs, and interoperability with native JavaScript types, and is compatible with ts-pattern.

Tokens
23.5K
Snippets
109
Records
117
Agent score
73%

What's inside @bloodyowl/boxed

  1. Overview of Boxed core concepts

    main

    Boxed provides essential building blocks designed to handle common application states that typically lead to complex code and subtle bugs in standard JavaScript/TypeScript. It focuses on three primary states:

    • optionality: Managing whether a value is present or absent.
    • success: Managing values that can be computed or may fail to be computed.
    • completion: Managing whether a value is currently available or not.

    Instead of using standard null/undefined checks or try/catch blocks which can lead to logic errors, Boxed uses mathematical data structures (Monads and Functors) to provide a safer, more predictable way to compose logic and handle these states.

  2. Overview of @bloodyowl/boxed building blocks

    main

    @bloodyowl/boxed provides essential types and functions designed for writing functional and safe TypeScript code. The library focuses on immutability, developer experience (via chaining APIs), and interoperability with native JavaScript types. It is also compatible with ts-pattern using provided patterns.

    Core types included in the library:

    • Option<Value>: For handling optional values without null or undefined checks.
    • Result<Ok, Error>: For error handling and representing operations that can fail.
    • Future<Value>: For representing eventual values.
    • AsyncData<Value>: For managing asynchronous data states.
    • Lazy<Value>: For deferred value computation.
    • Utilities: Includes Deferred, Dict, and Array utilities.
  3. What is a Future and how does it differ from a Promise?

    main

    A Future is a replacement for Promise in @bloodyowl/boxed. While you can still await a Future, it differs from a standard Promise in several key ways:

    • Error Handling: Instead of a rejection state, Futures use a contained Result type to handle errors.
    • Cancellation: Futures have built-in cancellation support. Unlike the fetch signal API, cancelling a Future does not cause it to reject.
    • Composition: Futures do not "swallow" other futures returned from .map() or .flatMap().
    • Execution: Future callbacks run synchronously.

    Use Future when you need built-in cancellation or want to avoid the implicit rejection patterns of standard Promises.

  4. What is AsyncData<Value> and how to use it

    main

    The AsyncData<Value> type represents the state of an asynchronous flow (like a network request) as a discriminating union. This avoids manual state management for loading flows by explicitly representing three possible states:

    • NotAsked: The operation has not been initiated.
    • Loading: The operation is currently in progress.
    • Done(value): The operation has completed successfully with a specific value.

    Since v3.0.0, AsyncData values are referentially equal if they contain the same value (e.g., AsyncData.Done(1) === AsyncData.Done(1)).

    import { AsyncData } from "@bloodyowl/boxed"
    
    const notAsked = AsyncData.NotAsked()
    const loading = AsyncData.Loading()
    const done = AsyncData.Done(1)
  5. How the Option type works

    main

    The Option<Value> type is a container used to represent optional data, serving as a safer alternative to null and undefined. It distinguishes between a value being present or absent, allowing you to represent states like Some(None) which is impossible with standard nullability.

    An Option can be in one of two states:

    • Some(value): The container holds a value.
    • None: The container is empty.

    Since v3.0.0, Option values are referentially equal if they contain the same value (e.g., Option.Some(1) === Option.Some(1)).

    import { Option } from "@bloodyowl/boxed"
    
    const aName = Option.Some("John")
    const bName = Option.None()
  6. Manage nested optionality using the Option type

    main

    Instead of manually checking for null or undefined at every step of a transformation pipeline, use the Option<T> type. This allows you to chain operations using map, flatMap, and getOr, which preserves the intent of the code and avoids tedious conditional logic.

    • Use .map(fn) when the function fn takes a value and returns a non-optional value.
    • Use .flatMap(fn) when the function fn takes a value and returns an Option<T>.
    • Use .getOr(fallback) to extract the final value or provide a default if any step in the chain resulted in an empty option.
    // Assuming input is an Option<string>
    input
      .map(parseInput)
      .flatMap(transform)
      .map(print)
      .map(prettify)
      .getOr("fallback")
  7. Understanding Eager vs Lazy execution in Boxed

    main

    Boxed prioritizes eager execution over lazy initialization. Most operations execute immediately when called, rather than requiring an explicit .run() or similar call at the end of a chain.

    If you require lazy behavior, Boxed recommends the following patterns:

    • Wrap the creation of the data structure in a function (e.g., () => Future.make(...)).
    • Use the Deferred mechanism to control when a value is resolved.
    // Eager: executes immediately
    const eagerFuture = Future.make((resolve) => resolve(1))
    
    // Lazy: execution is deferred until the function is called
    const lazyFuture = () => Future.make((resolve) => resolve(1))
    
    // Manual control via Deferred
    const [deferred, resolve] = Deferred.make<number>()
  8. Transform data using map and flatMap

    main

    Data manipulation in Boxed is primarily handled through two functions: map and flatMap.

    • map: Transforms the value inside the box using a callback function. If the box is empty (e.g., None), the transformation is skipped and the empty box is returned.
    • flatMap: Used when the transformation function itself returns a new box. This is essential for flattening nested optional values and avoiding types like Option<Option<T>>.
    const some = Option.Some(1)
    const none = Option.None()
    
    // map transforms the value
    const doubledSome = some.map((x) => x * 2) // Option.Some<2>
    const doubledNone = none.map((x) => x * 2) // Option.None
    
    // flatMap handles nested boxes
    type UserInfo = { name: Option<string> }
    type User = { id: string; info: Option<UserInfo> }
    
    const name = user
      .flatMap((user) => user.info) // Returns the Option<UserInfo>
      .flatMap((info) => info.name) // Returns the Option<string>
      .getOr("Anonymous user")
  9. What are Boxes and how to use them

    main

    In the Boxed library, data structures are conceptualized as boxes (containers) that may or may not contain a value. This abstraction allows you to work with data without knowing its exact state until you explicitly extract it, similar to the Schrödinger's cat thought experiment.

    To extract a value from a box, you can use methods like .getOr(fallback) to provide a default value, or .match({ Some, None }) to handle both possible states explicitly.

    // Assuming `option` is of type `Option<number>`
    
    // Returns the value if present or the fallback otherwise
    const a = option.getOr(0)
    
    // Explode the box
    const b = option.match({
      Some: (value) => value,
      None: () => 0,
    })
  10. How the Chaining API works in Boxed

    main

    Unlike many functional programming libraries that use a data-last API (map(func)(value)) combined with a pipe function, Boxed uses a chaining API.

    This design choice is intended to improve the developer experience in TypeScript by:

    1. Providing native autocomplete within editors.
    2. Making it easier to inspect values at different stages of a chain.
    3. Reducing the number of required imports.

    Instead of piping data through multiple functions, you call methods directly on the Boxed objects to transform or process data.

  11. Naming conventions in Boxed

    main

    Boxed follows JavaScript built-in naming conventions rather than abstract functional programming theory. If a concept exists in standard JavaScript (like Promise), Boxed will use similar terminology to make the API intuitive for JS/TS developers.

    For example, instead of using theoretical names like sequenceArray, Boxed uses Future.all to mirror Promise.all.

  12. Use AsyncData to manage React request states

    main

    Instead of manually managing isLoading, error, and data states in React—which can lead to impossible states and complex nested conditions—use the AsyncData type from @bloodyowl/boxed.

    AsyncData provides a type-safe way to represent the lifecycle of an asynchronous request. You can transition through states like NotAsked, Loading, and Done (or Error). To consume the data in your component, use the .match() method, which allows you to handle each state in a flat, readable way without manual conditional checks.

    import { useState, useEffect } from "react"
    import { AsyncData } from "@bloodyowl/boxed"
    import { queryUser, User } from "./api"
    
    type Props = {
      userId: string
    }
    
    const UserPage = ({ userId }: Props) => {
      // Initially, the request hasn't performed
      const [user, setUser] = useState(() => AsyncData.NotAsked<User>())
    
      useEffect(() => {
        // Indicate that we started loading
        setUser(AsyncData.Loading())
        const cancel = queryUser({ userId }, (user) => {
          // Then, set the received value
          setUser(AsyncData.Done(user))
        })
        return cancel
      }, [userId])
    
      // We can then match on the value, in a flat way
      return user.match({
        NotAsked: () => null,
        Loading: () => `Loading`,
        Done: (user) => `Hello ${user.name}!`,
      })
    }