Skip to content

📚Write safer TypeScript using Maybe, List, Result, and Either monads.

License

Notifications You must be signed in to change notification settings

patrickmichalina/typescript-monads

Repository files navigation

📚 typescript-monads

Better TypeScript Control Flow

circeci

semantic-release npm latest version

typescript-monads helps you write safer code by using abstractions over messy control flow and state.

Installation

You can use this library in the browser, node, or a bundler

Node or as a module

npm install typescript-monads

Browser

<head>
 <script src="https://unpkg.com/typescript-monads"></script>
 <!-- or use a specific version to avoid a http redirect --> 
 <script src="https://unpkg.com/typescript-monads@5.3.0/index.min.js"></script>
</head>
var someRemoteValue;
typescriptMonads.maybe(someRemoteValue).tapSome(console.log)

Example Usage

Maybe

The Maybe monad represents values that may or may not exist. It's a safe way to handle potentially null or undefined values without resorting to null checks throughout your code.

import { maybe, none } from 'typescript-monads'

// Creating Maybe instances
const someValue = maybe(42)        // Maybe with a value
const noValue = maybe(null)        // Maybe with no value (None)
const alsoNoValue = none<number>() // Explicitly create a None

// Safe value access
someValue.valueOr(0)           // 42
noValue.valueOr(0)             // 0
someValue.valueOrCompute(() => expensiveCalculation()) // 42 (computation skipped)
noValue.valueOrCompute(() => expensiveCalculation())   // result of computation

// Conditional execution with pattern matching
someValue.match({
  some: val => console.log(`Got a value: ${val}`),
  none: () => console.log('No value present')
}) // logs: "Got a value: 42"

// Side effects with tap
someValue.tap({
  some: val => console.log(`Got ${val}`),
  none: () => console.log('Nothing to see')
})

// Conditional side effects
someValue.tapSome(val => console.log(`Got ${val}`))
noValue.tapNone(() => console.log('Nothing here'))

// Chaining operations (only executed for Some values)
maybe(5)
  .map(n => n * 2)           // maybe(10)
  .filter(n => n > 5)        // maybe(10)
  .flatMap(n => maybe(n + 1)) // maybe(11)

// Transforming to other types
maybe(5).toResult('No value found') // Ok(5)
maybe(null).toResult('No value found') // Fail('No value found')

// Working with RxJS (with rxjs optional dependency)
import { maybeToObservable } from 'typescript-monads'
maybeToObservable(maybe(5)) // Observable that emits 5 and completes
maybeToObservable(none())   // Observable that completes without emitting

List

The List monad is a lazily evaluated collection with chainable operations. It provides many of the common list processing operations found in functional programming languages.

import { List } from 'typescript-monads'

// Creating Lists
const fromValues = List.of(1, 2, 3, 4, 5)
const fromIterable = List.from([1, 2, 3, 4, 5])
const numbersFromRange = List.range(1, 10)    // 1 to 10
const infiniteNumbers = List.integers()        // All integers (use with take!)
const empty = List.empty<number>()

// Basic operations
fromValues.toArray() // [1, 2, 3, 4, 5]
fromValues.headOrUndefined() // 1
fromValues.headOr(0) // 1
empty.headOr(0) // 0

// Transformations
fromValues
  .map(n => n * 2)                 // [2, 4, 6, 8, 10]
  .filter(n => n > 5)              // [6, 8, 10]
  .take(2)                         // [6, 8]
  .drop(1)                         // [8]

// LINQ-style operations
fromValues.sum()                    // 15
fromValues.all(n => n > 0)          // true
fromValues.any(n => n % 2 === 0)    // true
fromValues.where(n => n % 2 === 0)  // [2, 4]

// Conversion
fromValues.toDictionary()           // { 0: 1, 1: 2, 2: 3, 3: 4, 4: 5 }
const users = List.of(
  { id: 'a', name: 'Alice' },
  { id: 'b', name: 'Bob' }
)
users.toDictionary('id') // { 'a': { id: 'a', name: 'Alice' }, 'b': { id: 'b', name: 'Bob' } }

Either

The Either monad represents values that can be one of two possible types. It's often used to represent a value that can be either a success (Right) or a failure (Left).

import { either } from 'typescript-monads'

// Creating Either instances
const rightValue = either<string, number>(undefined, 42) // Right value
const leftValue = either<string, number>('error', undefined) // Left value

// Checking which side is present
rightValue.isRight() // true
rightValue.isLeft()  // false
leftValue.isRight()  // false 
leftValue.isLeft()   // true

// Pattern matching
rightValue.match({
  right: val => `Success: ${val}`,
  left: err => `Error: ${err}`
}) // "Success: 42"

// Side effects with tap
rightValue.tap({
  right: val => console.log(`Got right: ${val}`),
  left: err => console.log(`Got left: ${err}`)
})

// Transformation (only applies to Right values)
rightValue.map(n => n * 2) // Either with Right(84)
leftValue.map(n => n * 2)  // Either with Left('error') unchanged

// Chaining (flatMap only applies to Right values)
rightValue.flatMap(n => either(undefined, n + 10)) // Either with Right(52)
leftValue.flatMap(n => either(undefined, n + 10))  // Either with Left('error') unchanged

Reader

The Reader monad represents a computation that depends on some external configuration or environment. It's useful for dependency injection.

import { reader } from 'typescript-monads'

// Define a configuration type
interface Config {
  apiUrl: string
  apiKey: string
}

// Create readers that depend on this configuration
const getApiUrl = reader<Config, string>(config => config.apiUrl)
const getApiKey = reader<Config, string>(config => config.apiKey)

// Compose readers to build more complex operations
const getAuthHeader = getApiKey.map(key => `Bearer ${key}`)

// Create a reader for making an API request
const fetchData = reader<Config, Promise<Response>>(config => {
  return fetch(`${config.apiUrl}/data`, {
    headers: {
      'Authorization': `Bearer ${config.apiKey}`
    }
  })
})

// Execute the reader with a specific configuration
const config: Config = {
  apiUrl: 'https://api.example.com',
  apiKey: 'secret-key-123'
}

const apiUrl = getApiUrl.run(config)  // 'https://api.example.com'
const authHeader = getAuthHeader.run(config) // 'Bearer secret-key-123'
fetchData.run(config).then(response => {
  // Handle API response
})

Result

The Result monad represents operations that can either succeed with a value or fail with an error. It's similar to Either but with more specific semantics for success/failure.

import { ok, fail, catchResult } from 'typescript-monads'

// Creating Result instances
const success = ok<number, string>(42)        // Success with value 42
const failure = fail<number, string>('error') // Failure with error 'error'

// Safely catching exceptions
const result = catchResult<number, Error>(
  () => {
    // Code that might throw
    if (Math.random() > 0.5) {
      throw new Error('Failed')
    }
    return 42
  }
)

// Checking result type
success.isOk()   // true
success.isFail() // false
failure.isOk()   // false
failure.isFail() // true

// Extracting values safely
success.unwrapOr(0)   // 42
failure.unwrapOr(0)   // 0
success.unwrap()      // 42
// failure.unwrap()   // Throws error

// Convert to Maybe
success.maybeOk()    // Maybe with Some(42)
failure.maybeOk()    // Maybe with None
success.maybeFail()  // Maybe with None
failure.maybeFail()  // Maybe with Some('error')

// Pattern matching
success.match({
  ok: val => `Success: ${val}`,
  fail: err => `Error: ${err}`
}) // "Success: 42"

// Transformations
success.map(n => n * 2)          // Ok(84)
failure.map(n => n * 2)          // Fail('error') unchanged
failure.mapFail(e => `${e}!`)    // Fail('error!') 

// Chaining
success.flatMap(n => ok(n + 10)) // Ok(52)
failure.flatMap(n => ok(n + 10)) // Fail('error') unchanged

// Side effects
success.tap({
  ok: val => console.log(`Success: ${val}`),
  fail: err => console.log(`Error: ${err}`)
})
success.tapOk(val => console.log(`Success: ${val}`))
failure.tapFail(err => console.log(`Error: ${err}`))

// Chaining with side effects
success
  .tapOkThru(val => console.log(`Success: ${val}`))
  .map(n => n * 2)

// Converting to promises
import { resultToPromise } from 'typescript-monads'
resultToPromise(success) // Promise that resolves to 42
resultToPromise(failure) // Promise that rejects with 'error'

State

The State monad represents computations that can read and transform state. It's useful for threading state through a series of operations.

import { state } from 'typescript-monads'

// Define a state type
interface AppState {
  count: number
  name: string
}

// Initial state
const initialState: AppState = {
  count: 0,
  name: ''
}

// Create operations that work with the state
const incrementCount = state<AppState, number>(s => 
  [{ ...s, count: s.count + 1 }, s.count + 1]
)

const setName = (name: string) => state<AppState, void>(s => 
  [{ ...s, name }, undefined]
)

const getCount = state<AppState, number>(s => [s, s.count])

// Compose operations
const operation = incrementCount
  .flatMap(() => setName('Alice'))
  .flatMap(() => getCount)

// Run the state operation
const result = operation.run(initialState)
console.log(result.state)  // { count: 1, name: 'Alice' }
console.log(result.value)  // 1

Logger

The Logger monad lets you collect logs during a computation. It's useful for debugging or creating audit trails.

import { logger, tell } from 'typescript-monads'

// Create a logger with initial logs and value
const initialLogger = logger<string, number>(['Starting process'], 0)

// Add logs and transform value
const result = initialLogger
  .flatMap(val => {
    return logger(['Incrementing value'], val + 1)
  })
  .flatMap(val => {
    return logger(['Doubling value'], val * 2)
  })

// Extract all logs and final value
result.runUsing(({ logs, value }) => {
  console.log('Logs:', logs)    // ['Starting process', 'Incrementing value', 'Doubling value']
  console.log('Final value:', value) // 2
})

// Start with a single log entry
const startLogger = tell<string>('Beginning')

// Add a value to the logger
const withValue = logger.startWith<string, number>('Starting with value:', 42)

// Extract results using pattern matching
const output = result.runUsing(({ logs, value }) => {
  return { 
    history: logs.join('\n'),
    result: value 
  }
})

Integration with Other Libraries

RxJS Integration

This library offers RxJS integration with the Maybe and Result monads:

import { maybeToObservable } from 'typescript-monads'
import { resultToObservable } from 'typescript-monads'
import { of } from 'rxjs'
import { flatMap } from 'rxjs/operators'

// Convert Maybe to Observable
of(maybe(5)).pipe(
  flatMap(maybeToObservable)
).subscribe(val => console.log(val)) // logs 5 and completes

// Convert Result to Observable  
of(ok(42)).pipe(
  flatMap(resultToObservable)
).subscribe(
  val => console.log(`Success: ${val}`),
  err => console.error(`Error: ${err}`)
)

Promise Integration

You can convert Result monads to promises:

import { resultToPromise } from 'typescript-monads'

// Convert Result to Promise
resultToPromise(ok(42))
  .then(val => console.log(`Success: ${val}`))
  .catch(err => console.error(`Error: ${err}`))

// Catch exceptions and convert to Result
async function fetchData() {
  try {
    const response = await fetch('https://api.example.com/data')
    if (!response.ok) {
      throw new Error(`HTTP error ${response.status}`)
    }
    return ok(await response.json())
  } catch (error) {
    return fail(error)
  }
}

Contributing

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

License

MIT