Skip to content

AlexisMora/result-type

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

@alexismora/result-type

A Rust-inspired Result type for TypeScript that makes error handling explicit and unavoidable.

Installation

npm install @alexismora/result-type

Quick Start

import { Result, ok, err } from '@alexismora/result-type'

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return err("Division by zero")
  }
  return ok(a / b)
}

// Force explicit error handling
const result = divide(10, 2)
const value = result.expect("Division should succeed") // 5

// Safe handling with defaults
const safe = divide(10, 0).unwrapOr(0) // 0

Why implement Result?

Traditional Typescript error handling with try/catch is easy to ignore. The Result type forces you to handle errors explicitly, making your code more reliable and predictable.

Key Features

  • Rust-inspired API - Familiar to Rust developers
  • Type-safe - Full TypeScript support with strict inference
  • Zero dependencies - Lightweight and fast
  • Explicit errors - No more forgotten error handling
  • Functional - Chain operations with map, andThen, etc.
  • Dual packages - ESM and CommonJS support

Usage

Table of Contents

Overview

The Result type represents either success (Ok) or failure (Err). It forces you to handle errors explicitly, preventing the common JavaScript pattern of ignoring errors.

import { Result, ok, err } from '@alexismora/result-type'

type MyResult = Result<number, string>
const success: MyResult = ok(42)
const failure: MyResult = err("something went wrong")

Basic Types

Result<T, E>

A type that is either Ok<T> (success with value of type T) or Err<E> (failure with error of type E).

Ok<T, E>

Represents a successful result containing a value of type T.

Err<T, E>

Represents a failed result containing an error of type E.

Creating Results

ok<T, E>(value: T): Result<T, E>

Creates a successful Result containing the given value.

const result = ok(42)
// Result<number, never>

err<T, E>(error: E): Result<T, E>

Creates a failed Result containing the given error.

const result = err("File not found")
// Result<never, string>

Constructor Usage

You can also use the class constructors directly:

import { Ok, Err } from '@alexismora/result-type'

const success = new Ok(42)
const failure = new Err("error message")

Core Methods

isOk(): boolean

Returns true if the result is Ok. Acts as a type guard in TypeScript.

const result = ok(42)

if (result.isOk()) {
  // TypeScript knows this is Ok<number>
  console.log(result.value) // 42
}

isErr(): boolean

Returns true if the result is Err. Acts as a type guard in TypeScript.

const result = err("failed")

if (result.isErr()) {
  // TypeScript knows this is Err<string>
  console.log(result.error) // "failed"
}

expect(message: string): T

Returns the contained Ok value. If the result is Err, throws an error with your custom message.

Use this when you have a good reason to expect success and want to fail fast with a clear message.

const config = loadConfig().expect("Config file must exist")
// If loadConfig() returns Err, throws: "Config file must exist: [error details]"

unwrap(): T

Returns the contained Ok value. If the result is Err, throws with a default message.

Use sparingly - prefer expect() for better error messages or safe methods like unwrapOr().

const value = ok(42).unwrap() // 42
const value = err("failed").unwrap() // Throws: "Called unwrap on an Err value: failed"

unwrapOr(defaultValue: T): T

Returns the contained Ok value, or the provided default if Err.

Use this when you have a sensible fallback value.

const port = getPort().unwrapOr(3000)
// If getPort() fails, uses 3000 as default

unwrapOrElse(fn: (error: E) => T): T

Returns the contained Ok value, or computes a value from the error using the provided function.

Use this when you need to compute a fallback based on the error.

const value = result.unwrapOrElse((error) => {
  console.error("Operation failed:", error)
  return getDefaultValue()
})

Functional Methods

map<U>(fn: (value: T) => U): Result<U, E>

Transforms the Ok value by applying a function. If Err, returns the error unchanged.

Use for transforming successful values while preserving errors.

const result = ok(5)
  .map(x => x * 2)
  .map(x => x.toString())
// Result<string, never> = Ok("10")

const failed = err("oops").map(x => x * 2)
// Result<number, string> = Err("oops")

mapErr<F>(fn: (error: E) => F): Result<T, F>

Transforms the Err value by applying a function. If Ok, returns the value unchanged.

Use for transforming or enriching error information.

const result = err(404)
  .mapErr(code => `HTTP Error ${code}`)
// Result<never, string> = Err("HTTP Error 404")

const success = ok(42).mapErr(e => `Error: ${e}`)
// Result<number, string> = Ok(42)

andThen<U>(fn: (value: T) => Result<U, E>): Result<U, E>

Chains operations that return Results. Also known as flatMap.

Use for sequential operations that can each fail.

const result = readFile("config.json")
  .andThen(content => parseJSON(content))
  .andThen(config => validateConfig(config))

// If any step fails, the error propagates
// If all succeed, you get the final value

or(other: Result<T, E>): Result<T, E>

Returns this result if Ok, otherwise returns the alternative.

Use for providing fallback operations.

const result = readFromCache()
  .or(readFromDatabase())
  .or(readFromAPI())
// Uses the first successful result

match<U>(patterns: { ok: (value: T) => U, err: (error: E) => U }): U

Pattern matching for Results. Calls one function or the other based on the variant.

Use for handling both cases explicitly and transforming to a common type.

const message = result.match({
  ok: (value) => `Success: ${value}`,
  err: (error) => `Failed: ${error}`
})

const status = apiCall().match({
  ok: (data) => ({ success: true, data }),
  err: (error) => ({ success: false, error })
})

Helper Functions

tryCatch<T, E>(fn: () => T, errorHandler?: (error: unknown) => E): Result<T, E>

Wraps a function that might throw an exception into a Result.

Use to convert exception-based code to Result-based code.

const result = tryCatch(
  () => JSON.parse(jsonString),
  (error) => `Parse error: ${error}`
)

if (result.isOk()) {
  console.log("Parsed:", result.value)
} else {
  console.error("Failed:", result.error)
}

Without error handler, the caught error is used directly:

const result = tryCatch(() => riskyOperation())
// Result<ReturnType, unknown>

tryCatchAsync<T, E>(fn: () => Promise<T>, errorHandler?: (error: unknown) => E): Promise<Result<T, E>>

Async version of tryCatch. Wraps an async function that might throw.

Use to convert promise rejections into Results.

const result = await tryCatchAsync(
  async () => await fetch(url).then(r => r.json()),
  (error) => `Network error: ${error}`
)

result.match({
  ok: (data) => console.log("Data:", data),
  err: (error) => console.error("Error:", error)
})

Examples

Basic Error Handling

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) {
    return err("Division by zero")
  }
  return ok(a / b)
}

const result = divide(10, 2)
const value = result.expect("Division should succeed") // 5

const bad = divide(10, 0)
const safe = bad.unwrapOr(0) // 0

File Operations

function readConfig(): Result<Config, string> {
  return tryCatch(
    () => {
      const content = fs.readFileSync("config.json", "utf8")
      return JSON.parse(content)
    },
    (error) => `Failed to read config: ${error}`
  )
}

const config = readConfig()
  .map(c => ({ ...c, loaded: true }))
  .unwrapOr(getDefaultConfig())

API Calls

async function fetchUser(id: string): Promise<Result<User, ApiError>> {
  return tryCatchAsync(
    async () => {
      const response = await fetch(`/api/users/${id}`)
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}`)
      }
      return response.json()
    },
    (error) => ({
      code: "FETCH_ERROR",
      message: String(error)
    })
  )
}

// Usage
const result = await fetchUser("123")

const user = result.match({
  ok: (user) => user,
  err: (error) => {
    console.error("Failed to fetch user:", error)
    return null
  }
})

Chaining Operations

function processUserData(userId: string): Result<ProcessedData, string> {
  return fetchUserFromDB(userId)
    .andThen(user => validateUser(user))
    .andThen(user => enrichUserData(user))
    .andThen(data => processData(data))
    .mapErr(error => `User processing failed: ${error}`)
}

// If any step fails, the chain short-circuits and returns the error
// If all succeed, you get the final ProcessedData

Type-Safe Error Handling

type AppError =
  | { type: "NotFound"; resource: string }
  | { type: "Unauthorized"; reason: string }
  | { type: "Validation"; errors: string[] }

function getResource(id: string): Result<Resource, AppError> {
  if (!isAuthenticated()) {
    return err({ type: "Unauthorized", reason: "Not logged in" })
  }

  const resource = db.find(id)
  if (!resource) {
    return err({ type: "NotFound", resource: id })
  }

  return ok(resource)
}

const result = getResource("123")

result.match({
  ok: (resource) => displayResource(resource),
  err: (error) => {
    switch (error.type) {
      case "NotFound":
        show404(error.resource)
        break
      case "Unauthorized":
        redirectToLogin(error.reason)
        break
      case "Validation":
        showErrors(error.errors)
        break
    }
  }
})

Combining Multiple Results

function loadAppData(): Result<AppData, string> {
  const config = loadConfig()
  const user = loadUser()
  const settings = loadSettings()

  if (config.isErr()) return config
  if (user.isErr()) return user
  if (settings.isErr()) return settings

  return ok({
    config: config.value,
    user: user.value,
    settings: settings.value
  })
}

Graceful Degradation

const data = fetchFromCache()
  .or(fetchFromDatabase())
  .or(fetchFromAPI())
  .unwrapOr(getDefaultData())

// Tries cache first, then database, then API
// Falls back to default data if all fail

Best Practices

  1. Use expect() with descriptive messages when you have a good reason to believe the operation should succeed.

  2. Prefer unwrapOr() over unwrap() when you have a sensible default value.

  3. Use andThen() for chaining operations that can each fail - it's cleaner than nested if/else.

  4. Use match() for exhaustive handling when you need to handle both cases explicitly.

  5. Use typed errors (discriminated unions) for better error handling and IDE support.

  6. Wrap external APIs with tryCatch or tryCatchAsync to convert exceptions to Results.

  7. Don't mix paradigms - once you start using Result, avoid mixing it with try/catch in the same code path.

License

MIT

Contributing

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

Repository

https://github.com/AlexisMora/result-type

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

No packages published