Constructors

ok<T>(value: T): Ok<T, never>
Creates a success Result containing value. The error type defaults to never since an Ok can't hold an error.
const result = ok(42)        // Ok<number, never>
const user = ok({ name: 'alice' })  // Ok<{ name: string }, never>
err<E>(error: E): Err<never, E>
Creates a failure Result containing error. The success type defaults to never since an Err can't hold a value.
const result = err('not found')
const typed = err(new ResultError('TIMEOUT', 'request timed out'))
okAsync<T>(value: T): ResultAsync<T, never>
Creates an async success. Equivalent to ok(value).toAsync() but more concise. Useful when a function returns ResultAsync and you need to return a known-good value.
const result = okAsync(42)
const value = await result  // Ok<number, never>
errAsync<E>(error: E): ResultAsync<never, E>
Creates an async failure. Equivalent to err(error).toAsync(). Useful when a function returns ResultAsync and you need to return a known error.
const result = errAsync({ code: 'TIMEOUT', ms: 5000 })
fromThrowable<A, R, E>(fn: (...args: A) => R, errorFn: (e: unknown) => E): (...args: A) => Result<R, E>
Wraps a function that might throw into one that returns a Result. The errorFn parameter is required — this is how resultkit enforces typed errors at every boundary. If fn succeeds, you get Ok. If it throws, the caught error is passed to errorFn and you get Err.
const safeParse = fromThrowable(
  (s: string) => JSON.parse(s),
  (e) => new ResultError('PARSE', e instanceof Error ? e.message : 'unknown')
)

const result = safeParse('{"valid": true}')  // Ok
const bad = safeParse('nope')               // Err
fromThrowableAsync<A, R, E>(fn: (...args: A) => Promise<R>, errorFn: (e: unknown) => E): (...args: A) => ResultAsync<R, E>
Same as fromThrowable but for async functions. Wraps a function that returns a Promise into one that returns ResultAsync. Catches both synchronous throws and promise rejections.
const safeFetch = fromThrowableAsync(
  async (url: string) => {
    const r = await fetch(url)
    return r.json()
  },
  (e) => new ResultError('FETCH', String(e))
)

const result = await safeFetch('/api/users')
fromNullable<T, E>(value: T | null | undefined, error: E): Result<T, E>
Converts a nullable value into a Result. If value is null or undefined, returns Err(error). Otherwise returns Ok(value).
fromNullable(user.email, 'no email')   // Ok('alice@...') or Err('no email')
fromNullable(0, 'missing')              // Ok(0) — not Err
fromNullable(null, 'missing')           // Err('missing')
ResultAsync.fromPromise<T, E>(promise: PromiseLike<T>, errorFn: (e: unknown) => E): ResultAsync<T, E>
Wraps a promise that might reject into a ResultAsync. If the promise resolves, you get Ok. If it rejects, the rejection value is passed to errorFn and you get Err.
const result = ResultAsync.fromPromise(
  fetch('/api/data').then(r => r.json()),
  (e) => ({ code: 'NETWORK', message: String(e) })
)
ResultAsync.fromSafePromise<T>(promise: PromiseLike<T>): ResultAsync<T, never>
Wraps a promise that you guarantee will never reject. No errorFn needed because there's no rejection to handle. If the promise does reject, the rejection propagates as an unhandled rejection.
const config = ResultAsync.fromSafePromise(
  readFile('./config.json', 'utf-8')
)

Instance methods

Available on Ok, Err, and ResultAsync unless noted.

.isOk(): boolean   /   .isErr(): boolean
Type-narrowing guards. After calling .isOk() in an if block, TypeScript knows you have an Ok and can access .value. After .isErr(), you can access .error.
const result: Result<number, string> = divide(10, 2)

if (result.isOk()) {
  console.log(result.value)   // TypeScript knows this is number
}
if (result.isErr()) {
  console.log(result.error)   // TypeScript knows this is string
}
.map<U>(fn: (value: T) => U): Result<U, E>
Transforms the success value using fn. If the Result is an Err, the function is not called and the Err passes through unchanged.
ok(5).map(n => n * 2)           // Ok(10)
err('bad').map(n => n * 2)       // Err('bad') — fn not called
ok('hello').map(s => s.length)  // Ok(5)
.mapErr<F>(fn: (error: E) => F): Result<T, F>
Transforms the error value using fn. If the Result is an Ok, the function is not called and the Ok passes through unchanged.
err('bad').mapErr(e => e.toUpperCase())  // Err('BAD')
ok(5).mapErr(e => e.toUpperCase())      // Ok(5) — fn not called
.andThen<U, F>(fn: (value: T) => Result<U, F>): Result<U, E | F>
Chains into another Result-returning function. This is flatMap — use it when the transformation itself can fail. The error type becomes E | F because either the original error or the new error could occur.
function divide(a: number, b: number): Result<number, 'DIV_ZERO'> {
  return b === 0 ? err('DIV_ZERO') : ok(a / b)
}

ok(10).andThen(n => divide(n, 2))   // Ok(5)
ok(10).andThen(n => divide(n, 0))   // Err('DIV_ZERO')
err('bad').andThen(n => ok(n))     // Err('bad') — fn not called
.orElse<U, F>(fn: (error: E) => Result<U, F>): Result<T | U, F>
Attempts to recover from an error. The inverse of .andThen(). If the Result is Err, calls fn with the error to produce a new Result. If Ok, passes through unchanged.
err('bad').orElse(() => ok(0))    // Ok(0) — recovered
ok(5).orElse(() => ok(0))         // Ok(5) — fn not called
err('bad').orElse(() => err('worse'))  // Err('worse')
.match<A, B>(okFn, errFn): A | B   or   .match({ ok, err }): A | B
Exhaustively handles both paths and returns a value. Two calling styles: positional arguments or an object with ok and err keys. Both are equivalent.
// Positional style
result.match(
  value => `got ${value}`,
  error => `failed: ${error}`
)

// Object style — more readable in complex cases
result.match({
  ok:  value => renderSuccess(value),
  err: error => renderError(error)
})
.tap(fn: (value: T) => void): this   /   .tapErr(fn: (error: E) => void): this
Inspect the value or error without transforming it. The function's return value is ignored and the original Result is returned unchanged. Use for logging, metrics, or other side effects mid-chain.
ok(5)
  .tap(v => console.log('value:', v))     // logs "value: 5"
  .map(v => v * 2)                          // Ok(10)

err('bad')
  .tapErr(e => console.error('error:', e))  // logs "error: bad"
  .orElse(() => ok(0))                      // Ok(0)
.unwrapOr<A>(defaultValue: A): T | A
Extracts the success value, or returns defaultValue if the Result is an Err. This is a safe way to exit a Result chain when you have a sensible default.
ok(5).unwrapOr(0)           // 5
err('bad').unwrapOr(0)      // 0
.unwrap(): T   /   .unwrapErr(): E
Unsafe extraction. .unwrap() returns the value if Ok, or throws if Err. .unwrapErr() returns the error if Err, or throws if Ok. Both include the original value/error as Error.cause for debugging. .unwrap() additionally appends string errors to the message.
ok(5).unwrap()         // 5
err('bad').unwrap()    // throws Error('Called unwrap on an Err value: bad')
                        //   cause: 'bad'

err('bad').unwrapErr()  // 'bad'
ok(5).unwrapErr()      // throws Error('Called unwrapErr on an Ok value')
                        //   cause: 5
.toAsync(): ResultAsync<T, E>
Converts a sync Result into a ResultAsync. Use when you need to chain a sync Result into an async pipeline.
const sync = ok(5)
const async = sync.toAsync()  // ResultAsync<number, never>

// Now you can chain with async operations
const result = await sync.toAsync()
  .andThen(n => fetchUser(n))

Standalone functions

match<V extends string>(value: V, handlers: { [K in V]: (value: K) => R }): R
Pattern match on a string union type. Pass a value and an object of handlers — one per variant. TypeScript enforces that every variant has a handler (exhaustive) unless you include a _ wildcard.
type Color = 'red' | 'green' | 'blue'

// Exhaustive — all variants required
match(color, {
  red:   () => '#ff0000',
  green: () => '#00ff00',
  blue:  () => '#0000ff'
})

// Wildcard — handle some, catch-all the rest
match(color, {
  red: () => 'danger',
  _:   () => 'safe'
})
matchOn<T, K>(obj: T, discriminant: K, handlers: { ... }): R
Pattern match on a discriminated union using a key. Pass an object, the name of its discriminant property, and handlers for each variant. TypeScript narrows the type inside each handler so you get access to variant-specific properties.
type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rect';   w: number; h: number }

matchOn(shape, 'kind', {
  circle: s => Math.PI * s.radius ** 2,  // s: { kind, radius }
  rect:   s => s.w * s.h                  // s: { kind, w, h }
})
combine(results: readonly [...Results]): Result<[T1, T2, ...], E1 | E2 | ...>
Merges an array of Results into a single Result containing a tuple of all success values. If any Result is Err, returns the first Err encountered. The type system infers tuple types and unions all error types.
combine([ok(1), ok('hello')] as const)
// Ok<[number, string], never>  →  Ok([1, 'hello'])

combine([ok(1), err('bad'), ok(3)])
// Err('bad')  →  first error wins
combineAsync(results: readonly [...ResultAsyncs]): ResultAsync<[T1, ...], E1 | ...>
Async variant of combine. Uses Promise.all internally — all ResultAsyncs run concurrently and ordering matches the input array. Same first-error-wins behavior and tuple type inference. Note: all promises are awaited even if an early one is Err (Promise.all semantics), but only the first Err is returned.
const result = await combineAsync([
  fetchUser(1),
  fetchConfig()
] as const)
// Result<[User, Config], UserErr | ConfigErr>
partition<T, E>(results: readonly Result<T, E>[]): [T[], E[]]
Splits an array of Results into two arrays: success values and error values. Unlike combine, this processes every result — it doesn't short-circuit.
const results = [ok(1), err('bad'), ok(3), err('worse')]
const [oks, errs] = partition(results)
// oks:  [1, 3]
// errs: ['bad', 'worse']
flatten<T, E1, E2>(result: Result<Result<T, E2>, E1>): Result<T, E1 | E2>
Unwraps a nested Result. If you have a Result<Result<T, E2>, E1>, this collapses it to Result<T, E1 | E2>. Useful when you accidentally end up with double-wrapped results.
flatten(ok(ok(42)))           // Ok(42)
flatten(ok(err('inner')))     // Err('inner')
flatten(err('outer'))          // Err('outer')

Errors & utilities

class ResultError<Code extends string> extends Error
A branded error class designed for use with matchOn. Extends Error with a typed code property and optional context data. The Code generic makes it possible to define discriminated union error types.
type AppError =
  | ResultError<'NOT_FOUND'>
  | ResultError<'TIMEOUT'>

const e = new ResultError('NOT_FOUND', 'User not found', { userId: 123 })
e.code     // 'NOT_FOUND' (typed as literal)
e.message  // 'User not found'
e.context  // { userId: 123 }
isResultError(value: unknown): value is ResultError<string>
Type guard that narrows unknown to ResultError. Uses instanceof under the hood.
if (isResultError(caught)) {
  console.log(caught.code, caught.message)  // typed access
}
resultToJSON<T, E>(result: Result<T, E>): ResultJSON<T, E>
Serializes a Result into a plain object with a _tag discriminant. The output is safe to pass through JSON.stringify for crossing process boundaries (API responses, message queues, etc).
resultToJSON(ok(42))        // { _tag: 'Ok', value: 42 }
resultToJSON(err('bad'))   // { _tag: 'Err', error: 'bad' }

// Round-trip
const wire = JSON.stringify(resultToJSON(result))
resultFromJSON<T, E>(json: Record<string, unknown>): Result<T, E>
Deserializes a plain object back into a Result. Expects the _tag format produced by resultToJSON. Throws if the _tag is missing or invalid.
const data = JSON.parse(wire)
const result = resultFromJSON<User, AppError>(data)
type InferOkType<R>   /   type InferErrType<R>
Utility types that extract the success or error type from a Result or ResultAsync. Useful when you need to reference the inner types without manually spelling them out.
type MyResult = Result<number, string>

type T = InferOkType<MyResult>    // number
type E = InferErrType<MyResult>   // string

// Also works with ResultAsync
type T2 = InferOkType<ResultAsync<User, AppError>>  // User

ESLint preset

import resultkit from '@valencets/resultkit/eslint'
resultkit ships three eslint flat-config presets. strict (errors) and recommended (warnings) enforce the core error-handling rules. opinionated adds strict plus stylistic rules like banning export default.
// eslint.config.js
import resultkit from '@valencets/resultkit/eslint'

export default [
  ...resultkit.strict,    // or resultkit.recommended for warnings
  // your overrides here
]
Rules enforced: