@bfra.me/es

High-quality reusable types and utilities for ES development (JavaScript and TypeScript). Zero runtime dependencies for core utilities, tree-shakeable via subpath exports.
Features
- 🎯 Result Type — Discriminated union for type-safe error handling without exceptions
- 🔧 Functional Utilities —
pipe, compose, curry, memoize with full TypeScript inference
- 📦 Module Interop — ES/CommonJS interoperability helpers
- ⏱️ Async Utilities —
retry, timeout, debounce, throttle, concurrency control
- 🏷️ Branded Types — Compile-time type safety with zero runtime cost
- ✅ Validation — Path validation, input sanitization, common validators
- 🔍 Environment Detection — CI, editor, git lifecycle detection
- 📁 File Watcher — Debounced file watching with change detection (optional chokidar peer)
- ⚠️ Error Utilities — Structured errors with codes, context, and cause chain
Installation
pnpm add @bfra.me/es
npm install @bfra.me/es
yarn add @bfra.me/es
Optional Peer Dependencies
pnpm add chokidar
Quick Start
import {err, isOk, ok, pipe, retry} from '@bfra.me/es'
function divide(a: number, b: number) {
return b === 0 ? err(new Error('Division by zero')) : ok(a / b)
}
const result = divide(10, 2)
if (isOk(result)) {
console.log(result.data)
}
const transform = pipe(
(x: number) => x + 1,
(x: number) => x * 2
)
transform(5)
const data = await retry(() => fetch('/api/data'), {maxAttempts: 3})
Subpath Exports
All utilities are organized into tree-shakeable subpath exports. Import only what you need:
@bfra.me/es/result | Result type for error handling |
@bfra.me/es/functional | Functional programming utilities |
@bfra.me/es/async | Async utilities (retry, timeout, debounce) |
@bfra.me/es/module | ES module interoperability |
@bfra.me/es/types | Branded types and type guards |
@bfra.me/es/validation | Path validation and sanitization |
@bfra.me/es/error | Structured error utilities |
@bfra.me/es/env | Environment detection |
@bfra.me/es/watcher | File watcher abstraction |
API Reference
Result Type (@bfra.me/es/result)
A discriminated union type for error handling without exceptions. Inspired by Rust's Result type.
import type {Err, Ok, Result} from '@bfra.me/es/result'
import {err, flatMap, isErr, isOk, map, ok, unwrap, unwrapOr} from '@bfra.me/es/result'
Creating Results
const success = ok(42)
const failure = err(new Error('failed'))
Type Guards for Results
const result = divide(10, 2)
if (isOk(result)) {
console.log(result.data)
}
if (isErr(result)) {
console.error(result.error)
}
Transforming Results
const doubled = map(ok(5), x => x * 2)
const mapped = map(err('fail'), x => x * 2)
const parsed = flatMap(ok('42'), str => {
const num = Number.parseInt(str, 10)
return Number.isNaN(num) ? err('Invalid number') : ok(num)
})
const withContext = mapErr(err('not found'), e => new Error(`Resource ${e}`))
const value = unwrap(ok(42))
const willThrow = unwrap(err('fail'))
const withDefault = unwrapOr(err('fail'), 0)
Wrapping Throwing Code
import {fromPromise, fromThrowable} from '@bfra.me/es/result'
const parsed = fromThrowable(() => JSON.parse(input))
const fetched = await fromPromise(fetch('/api/data'))
Functional Utilities (@bfra.me/es/functional)
import {compose, constant, curry, flip, identity, memoize, noop, partial, pipe, tap} from '@bfra.me/es/functional'
pipe
Composes functions left-to-right. Each function receives the result of the previous.
const addOne = (x: number) => x + 1
const double = (x: number) => x * 2
const toString = (x: number) => `Value: ${x}`
const transform = pipe(addOne, double, toString)
transform(5)
compose
Composes functions right-to-left (mathematical composition order).
const addOneThenDouble = compose(double, addOne)
addOneThenDouble(5)
curry
Transforms a function to accept arguments one at a time.
const add = (a: number, b: number, c: number) => a + b + c
const curriedAdd = curry(add)
curriedAdd(1)(2)(3)
curriedAdd(1, 2)(3)
curriedAdd(1)(2, 3)
curriedAdd(1, 2, 3)
memoize
Caches function results based on arguments. Supports multiple cache strategies.
import {memoize} from '@bfra.me/es/functional'
const expensive = memoize((n: number) => {
return n * 2
})
const withLRU = memoize(fn, {
strategy: 'lru',
maxSize: 100
})
const withTTL = memoize(fn, {
strategy: 'ttl',
ttl: 60000
})
console.log(expensive.getStats())
expensive.clear()
Other Utilities
const x = identity(42)
const logged = pipe(
addOne,
tap(x => console.log('After addOne:', x)),
double
)
const greet = (greeting: string, name: string) => `${greeting}, ${name}!`
const sayHello = partial(greet, 'Hello')
sayHello('World')
const divide = (a: number, b: number) => a / b
const flipped = flip(divide)
flipped(2, 10)
const alwaysTrue = constant(true)
alwaysTrue()
element.addEventListener('click', noop)
Async Utilities (@bfra.me/es/async)
import {debounce, pAll, pLimit, retry, sleep, throttle, timeout} from '@bfra.me/es/async'
retry
Retries a function with exponential backoff.
const result = await retry(
() => fetch('/api/data'),
{
maxAttempts: 3,
initialDelay: 100,
maxDelay: 10000,
backoffFactor: 2,
shouldRetry: (error, attempt) => {
return error.message !== 'Not Found'
}
}
)
if (isOk(result)) {
console.log(result.data)
} else {
console.error('All retries failed:', result.error)
}
timeout
Wraps a promise with a timeout.
const result = await timeout(fetch('/api/slow'), 5000)
if (isErr(result) && result.error instanceof TimeoutError) {
console.log('Request timed out')
}
debounce
Creates a debounced function that delays invocation.
const saveInput = debounce((value: string) => {
localStorage.setItem('draft', value)
}, 300)
input.addEventListener('input', e => saveInput(e.target.value))
saveInput.cancel()
throttle
Limits function invocation frequency.
const handleScroll = throttle(() => {
updateScrollPosition()
}, 100)
window.addEventListener('scroll', handleScroll)
Concurrency Control
const limit = pLimit(5)
const results = await Promise.all(
urls.map(url => limit(() => fetch(url)))
)
const allResults = await pAll(
urls.map(url => () => fetch(url)),
{concurrency: 5}
)
Module Interop (@bfra.me/es/module)
import {dynamicImport, interopDefault, isESModule, isPackageInScope, resolveModule} from '@bfra.me/es/module'
interopDefault
Unwraps default exports from both ES and CommonJS modules.
const lodash = await interopDefault(import('lodash'))
const config = await interopDefault(import('./config.js'))
resolveModule
Safely resolves and imports a module with Result return type.
const result = await resolveModule<typeof import('lodash')>('lodash')
if (isOk(result)) {
const _ = result.data
}
isPackageInScope
Checks if a package is available from a specific directory context.
if (isPackageInScope('typescript', {scopeUrl: import.meta.url})) {
}
Type Utilities (@bfra.me/es/types)
import type {Brand, NonEmptyString, Opaque, PositiveInteger} from '@bfra.me/es/types'
import {assertType, brand, hasProperty, isArray, isNonNullable, isNumber, isObject, isString, unbrand} from '@bfra.me/es/types'
Branded Types
Create nominal types in TypeScript's structural type system.
type UserId = Brand<string, 'UserId'>
type OrderId = Brand<string, 'OrderId'>
function getUser(id: UserId): User { }
function getOrder(id: OrderId): Order { }
const userId = brand<string, 'UserId'>('user-123')
const orderId = brand<string, 'OrderId'>('order-456')
getUser(userId)
getUser(orderId)
Type Guards
function processValue(value: unknown) {
if (isString(value)) {
return value.toUpperCase()
}
if (isNumber(value)) {
return value * 2
}
if (isObject(value)) {
return Object.keys(value)
}
if (isArray(value)) {
return value.length
}
}
if (hasProperty(obj, 'name')) {
console.log(obj.name)
}
const values = [1, null, 2, undefined, 3].filter(isNonNullable)
Type Assertions
assertType(value, isString)
const isUser = (v: unknown): v is User =>
isObject(v) && hasProperty(v, 'name') && hasProperty(v, 'email')
assertType(data, isUser)
Validation (@bfra.me/es/validation)
import {createValidator, isWithinBoundary, sanitizeInput, validatePath} from '@bfra.me/es/validation'
Path Validation
Validates paths against traversal attacks.
const result = validatePath('../etc/passwd')
if (isErr(result)) {
console.log(result.error.code)
}
const valid = validatePath('src/index.ts')
if (isOk(valid)) {
}
if (isWithinBoundary('/app/uploads/file.txt', '/app/uploads')) {
}
Input Sanitization
const userInput = '<script>alert("xss")</script>'
const safe = sanitizeInput(userInput)
const cleaned = sanitizeInput(input, {
escapeHtml: true,
removeNullBytes: true,
trim: true
})
Error Utilities (@bfra.me/es/error)
import {BaseError, createError, formatError, NotFoundError, PermissionError, TimeoutError, ValidationError, withErrorContext} from '@bfra.me/es/error'
Structured Errors
const error = createError('User not found', {
code: 'USER_NOT_FOUND',
cause: originalError,
context: {userId: 123}
})
throw new ValidationError('Invalid email format', {
field: 'email',
value: 'not-an-email'
})
throw new NotFoundError('Resource not found', {
resourceType: 'User',
resourceId: '123'
})
Error Formatting
try {
riskyOperation()
} catch (error) {
console.log(formatError(error))
}
Environment Detection (@bfra.me/es/env)
import {getEnvironment, isBrowser, isDeno, isInCI, isInEditorEnv, isInGitLifecycle, isNode} from '@bfra.me/es/env'
if (isInCI()) {
}
if (isInEditorEnv()) {
}
if (isInGitLifecycle()) {
}
if (isNode()) { }
if (isBrowser()) { }
if (isDeno()) { }
const env = getEnvironment()
File Watcher (@bfra.me/es/watcher)
Note: Requires chokidar as a peer dependency.
import {createChangeDetector, createDebouncer, createFileHasher, createFileWatcher} from '@bfra.me/es/watcher'
Basic File Watching
const watcher = createFileWatcher(['src/**/*.ts', 'test/**/*.ts'], {
debounceMs: 100,
ignored: ['**/node_modules/**'],
usePolling: false
})
watcher.on('change', event => {
console.log('Changes detected:', event.changes)
})
await watcher.start()
await watcher.close()
Change Detection with Hashing
const hasher = createFileHasher('sha256')
const detector = createChangeDetector()
const hash = await hasher.hashFile('/path/to/file.ts')
const {changed, added, removed} = await detector.detectChanges({
'src/index.ts': 'abc123...',
})
TypeScript Configuration
This package requires TypeScript 5.0+ and works best with strict mode:
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": true,
"moduleResolution": "bundler"
}
}
Bundle Size
- Core utilities (excluding watcher): < 5KB minified
- Full package with watcher: < 10KB minified
- Tree-shaking supported via subpath exports
Requirements
- Node.js 20+
- TypeScript 5.0+ (for development)
- ES2022+ compatible runtime
Related Packages
License
MIT © Marcus R. Brown