0.1.1 • Published 12 months ago

@railway-ts/core v0.1.1

Weekly downloads
-
License
MIT
Repository
github
Last release
12 months ago

@railway-ts/core

npm version Build Status License: MIT Bundle Size TypeScript Coverage

A small, practical library for functional programming in TypeScript, focused on Railway Oriented Programming patterns.

Overview

@railway-ts/core provides robust abstractions for handling operations that might fail (Result) and values that might not exist (Option). These patterns help you write more predictable code by forcing the explicit handling of edge cases.

The library stays intentionally small with a focused API, embracing TypeScript's type system to enhance your development experience without adding unnecessary complexity.

Installation

# npm
npm install @railway-ts/core

# yarn
yarn add @railway-ts/core

# pnpm
pnpm add @railway-ts/core

# bun
bun add @railway-ts/core

Features

  • Option: A type-safe way to handle optional values without null or undefined
  • Result<T, E>: Error handling without exceptions
  • Utility functions: Functional composition tools like pipe() and flow()
  • Fully typed: Built with TypeScript for a great developer experience
  • Tree-shakable: Import only what you need
  • Zero dependencies: Lightweight with no external runtime dependencies
  • Module-based architecture: Access the entire API or just the parts you need

Comparison with Similar Libraries

@railway-ts/core vs neverthrow

  • Option Support: @railway-ts/core includes both Result and Option types, while neverthrow only provides Result
  • Tree Shaking: @railway-ts/core is fully tree-shakable with individual function imports; neverthrow uses namespaces which limit tree-shaking capabilities
  • Bundle Size: @railway-ts/core is likely smaller when tree-shaken due to its more modular architecture
  • API Design: @railway-ts/core uses standalone utility functions; neverthrow employs method chaining via namespaces
  • Module Structure: @railway-ts/core provides direct module imports (@railway-ts/core/option) for further optimization

@railway-ts/core vs fp-ts

  • Learning Curve: @railway-ts/core offers a simpler, more approachable API focused on practical use cases
  • Scope: @railway-ts/core is specifically designed for Option/Result patterns; fp-ts is a comprehensive functional programming ecosystem
  • Type System: @railway-ts/core leverages TypeScript's type system without advanced higher-kinded type simulations
  • Bundle Size: @railway-ts/core is significantly lighter than fp-ts when only Option/Result functionality is needed
  • Philosophy: @railway-ts/core prioritizes pragmatic simplicity; fp-ts emphasizes theoretical correctness

@railway-ts/core vs ts-results

  • API Style: @railway-ts/core uses standalone utility functions; ts-results primarily uses class-based implementation
  • Function Composition: @railway-ts/core includes pipe/flow utilities for cleaner, more readable composition
  • Bundle Size: Both are relatively lightweight, but @railway-ts/core has better tree-shaking
  • Type Safety: Both provide robust type safety, with @railway-ts/core focusing on pattern matching and function composition

@railway-ts/core vs purify-ts

  • Naming: @railway-ts/core uses Option/Result vs purify's Maybe/Either (more familiar to Rust developers)
  • Complexity: @railway-ts/core maintains a simpler API with fewer abstractions than purify-ts
  • Integration: @railway-ts/core provides dedicated utilities for integrating with Promise-based code and handling exceptions
  • Philosophy: @railway-ts/core focuses specifically on railway-oriented programming patterns, while purify-ts covers broader functional programming concepts

Usage

Option Type

Option is a type that represents an optional value: it's either Some(value) or None.

import { some, none, mapOption, matchOption } from "@railway-ts/core";
// Or directly from the module:
// import { some, none, map, match } from '@railway-ts/core/option';

// Creating Options
const withValue = some(42);
const empty = none<number>();

// Transforming Options
const doubled = mapOption(withValue, (x) => x * 2); // some(84)

// Pattern matching
const message = matchOption(doubled, {
  some: (value) => `Got value: ${value}`,
  none: () => "Got nothing",
}); // "Got value: 84"

// Safe handling of nullable values
import { fromNullableOption } from "@railway-ts/core";
const maybeUser = fromNullableOption(getUser()); // Returns Option<User>

Result Type

Result is a type that represents either success (Ok) or failure (Err).

import { ok, err, mapResult, matchResult } from "@railway-ts/core";
// Or directly from the module:
// import { ok, err, map, match } from '@railway-ts/core/result';

// Creating Results
const success = ok<number, string>(42);
const failure = err<string>("Something went wrong");

// Transforming Results
const doubled = mapResult(success, (x) => x * 2); // ok(84)

// Pattern matching
const message = matchResult(doubled, {
  ok: (value) => `Success: ${value}`,
  err: (error) => `Error: ${error}`,
}); // "Success: 84"

// Working with async operations
import { fromPromise } from "@railway-ts/core";

async function fetchData() {
  const result = await fromPromise(fetch("https://api.example.com/data"));
  return matchResult(result, {
    ok: (response) => handleResponse(response),
    err: (error) => handleError(error),
  });
}

Function Composition

Compose functions with pipe and flow:

import { pipe, flow, some, mapOption, filterOption } from "@railway-ts/core";

// pipe: process value through a series of functions
const result = pipe(
  5,
  (n) => n * 2, // 10
  (n) => n + 1, // 11
  (n) => n.toString(), // "11"
);

// flow: create a new function from a composition
const processNumber = flow(
  (n: number) => n * 2,
  (n) => n + 1,
  (n) => n.toString(),
);

const processed = processNumber(5); // "11"

// Works seamlessly with Option and Result
const processOption = flow(
  (n: number) => some(n),
  (opt) => mapOption(opt, (n) => n * 2),
  (opt) => filterOption(opt, (n) => n > 5),
);

const optionResult = processOption(10); // some(20)

API Documentation

Option Module

The Option<T> type represents a value that may or may not exist:

export const OPTION_BRAND = Symbol("OPTION_BRAND");

export type Option<T> =
  | {
      readonly some: true;
      readonly value: T;
      readonly [OPTION_BRAND]: "some";
    }
  | {
      readonly some: false;
      readonly [OPTION_BRAND]: "none";
    };

Core functions:

FunctionDescription
some<T>(value: T)Creates an Option containing a value
none<T>()Creates an empty Option
isSome<T>(option: Option<T>)Type guard for Some variant
isNone<T>(option: Option<T>)Type guard for None variant
map<T, U>(option: Option<T>, fn: (value: T) => U)Transforms the value if present
flatMap<T, U>(option: Option<T>, fn: (value: T) => Option<U>)Chain operations that return Options
filter<T>(option: Option<T>, predicate: (value: T) => boolean)Returns None if predicate fails
unwrap<T>(option: Option<T>, errorMsg?: string)Get the value or throw error
unwrapOr<T>(option: Option<T>, defaultValue: T)Get the value or return default
unwrapOrElse<T>(option: Option<T>, defaultFn: () => T)Get value or compute default
fromNullable<T>(value: T \| null \| undefined)Convert nullable to Option
combine<T>(options: Option<T>[])Combine multiple Options
match<T, R>(option, patterns)Pattern match on Option
tap<T>(option: Option<T>, fn: (value: T) => void)Execute side effect if Some
mapToResult<T, E>(option: Option<T>, error: E)Convert Option to Result

Result Module

The Result<T, E> type represents an operation that may succeed or fail:

export const RESULT_BRAND = Symbol("RESULT_BRAND");

export type Result<T, E> =
  | {
      readonly ok: true;
      readonly value: T;
      readonly [RESULT_BRAND]: "ok";
    }
  | {
      readonly ok: false;
      readonly error: E;
      readonly [RESULT_BRAND]: "error";
    };

Core functions:

FunctionDescription
ok<T, E>(value: T)Creates a successful Result
err<E>(error: E)Creates a failed Result
isOk<T, E>(result: Result<T, E>)Type guard for Ok variant
isErr<T, E>(result: Result<T, E>)Type guard for Err variant
map<T, E, U>(result: Result<T, E>, fn: (value: T) => U)Transforms success value
mapErr<T, E, F>(result: Result<T, E>, fn: (error: E) => F)Transforms error value
flatMap<T, E, U>(result: Result<T, E>, fn: (value: T) => Result<U, E>)Chain operations that return Results
filter<T, E>(result: Result<T, E>, predicate: (value: T) => boolean, error: E)Returns Err if predicate fails
unwrap<T, E>(result: Result<T, E>, errorMsg?: string)Get value or throw error
unwrapOr<T, E>(result: Result<T, E>, defaultValue: T)Get value or return default
unwrapOrElse<T, E>(result: Result<T, E>, defaultFn: () => T)Get value or compute default
combine<T, E>(results: Result<T, E>[])Combine multiple Results (first error)
combineAll<T, E>(results: Result<T, E>[])Combine multiple Results (all errors)
match<T, E, R>(result, patterns)Pattern match on Result
tap<T, E>(result: Result<T, E>, fn: (value: T) => void)Execute side effect if Ok
tapErr<T, E>(result: Result<T, E>, fn: (error: E) => void)Execute side effect if Err
mapToOption<T, E>(result: Result<T, E>)Convert Result to Option
fromTry<T>(f: () => T)Wraps function that might throw in Result
fromPromise<T, E>(promise: Promise<T>, errorFn?)Wraps Promise in Result
toPromise<T, E>(result: Result<T, E>)Converts Result to Promise

Utility Module

FunctionDescription
pipe(value, ...fns)Passes value through functions left to right
flow(...fns)Creates a new function from composition

Advanced Examples

Using Option for null-safety

import { fromNullableOption, matchOption } from "@railway-ts/core";

function getUserName(userId: string) {
  const user = findUser(userId); // might return null

  return pipe(
    fromNullableOption(user),
    (opt) => mapOption(opt, (user) => user.name),
    (opt) =>
      matchOption(opt, {
        some: (name) => name,
        none: () => "Guest",
      }),
  );
}

Error handling with Result

import { fromTry, mapResult, matchResult } from "@railway-ts/core";

function parseJSON(input: string) {
  return fromTry(() => JSON.parse(input));
}

function processData(input: string) {
  return pipe(
    parseJSON(input),
    (result) => mapResult(result, (data) => transformData(data)),
    (result) =>
      matchResult(result, {
        ok: (data) => ({ success: true, data }),
        err: (error) => ({ success: false, error: error.message }),
      }),
  );
}

Converting between Option and Result

import {
  some,
  none,
  fromNullableOption,
  mapToResult,
  ok,
  err,
  mapToOption,
  pipe,
  flatMapResult,
} from "@railway-ts/core";

// Example: User authentication flow
type User = { id: string; name: string };
type AuthError = { code: number; message: string };

// 1. Converting Option to Result
function findUserById(id: string): Option<User> {
  // Database lookup that might return null
  const user = getUserFromDb(id);
  return fromNullableOption(user);
}

function authenticateUser(id: string): Result<User, AuthError> {
  // Convert Option<User> to Result<User, AuthError>
  return pipe(findUserById(id), (userOption) => mapToResult(userOption, { code: 404, message: "User not found" }));
}

// 2. Converting Result to Option
function validateToken(token: string): Result<string, AuthError> {
  if (!token) return err({ code: 401, message: "Token required" });
  if (!isValidFormat(token)) return err({ code: 400, message: "Invalid token format" });
  return ok(token);
}

function tryGetUserIdFromToken(token: string): Option<string> {
  // Convert Result<string, AuthError> to Option<string>
  // Discards the error information but preserves the success value
  return pipe(validateToken(token), (tokenResult) => mapToOption(tokenResult));
}

// 3. Combining both patterns in a workflow
async function getUserProfile(token: string): Promise<Result<User, AuthError>> {
  // First validate the token (Result-based validation)
  return pipe(
    validateToken(token),
    // Extract user ID from token if valid
    (tokenResult) =>
      flatMapResult(tokenResult, (token) => {
        const userId = extractUserId(token);

        // Find the user (Option-based retrieval)
        const userOption = findUserById(userId);

        // Convert Option to Result with appropriate error
        return mapToResult(userOption, { code: 404, message: "User profile not found" });
      }),
  );
}

// Usage
async function handleRequest(request) {
  const { token } = request;

  const profileResult = await getUserProfile(token);

  return matchResult(profileResult, {
    ok: (user) => ({ status: 200, body: user }),
    err: (error) => ({ status: error.code, body: { error: error.message } }),
  });
}

Async validation with Result

import { ok, err, fromPromise, flatMapResult } from "@railway-ts/core";

type ValidationError = { field: string; message: string };

function validateUsername(username: string): Result<string, ValidationError> {
  if (username.length < 3) {
    return err({ field: "username", message: "Too short" });
  }
  return ok(username);
}

async function checkUsernameAvailable(username: string): Promise<Result<string, ValidationError>> {
  const response = await fromPromise(fetch(`/api/check-username?username=${username}`));

  return flatMapResult(response, async (res) => {
    const data = await res.json();
    return data.available ? ok(username) : err({ field: "username", message: "Already taken" });
  });
}

async function registerUser(username: string) {
  return pipe(
    validateUsername(username),
    (result) => flatMapResult(result, (username) => checkUsernameAvailable(username)),
    // Continue registration process...
  );
}

Examples Documentation

For detailed examples of how to use the library:

Tree-Shaking and Module Structure

@railway-ts/core is designed to be tree-shakable. Import only what you need:

// Import everything with prefixes
import { some, mapOption } from "@railway-ts/core";

// Import directly from modules
import { some, map } from "@railway-ts/core/option";
import { ok, err } from "@railway-ts/core/result";
import { pipe, flow } from "@railway-ts/core/utils";

License

MIT

Contributing

Contributions are welcome! Please feel free to submit a Pull Request.


Inspired by functional programming concepts from languages like Rust, Scala, F#, and Haskell.

0.1.1

12 months ago

0.1.0

12 months ago