t-result
v0.6.1
Published
[](https://badge.fury.io/js/t-result)
Readme
T Result
A TypeScript implementation of the Result pattern, providing a robust way to handle operations that can succeed or fail, without relying on traditional try/catch blocks for expected errors.
Features
- Explicit Success/Failure: Clearly distinguish between
Ok<T>(success) andErr<E>(failure) states. - Type Safety: Strong typing for both success values (
T) and error types (E). - Chaining Methods: Fluent API with methods like
mapOk,mapErr,unwrapOr,onOk,onErrfor elegant data transformation and error handling. - Error Normalization: Utilities like
unknownToErrorto consistently convert various error types intoErrorobjects. - Asynchronous Support: Helpers for working with
Resulttypes in asynchronous operations (Result.asyncUnwrap,Result.asyncMap). - Function Wrapping:
resultifyfunction to easily convert existing functions or promises to returnResultobjects. - Typed Helpers:
getOkErrto create type-safeokanderrconstructors for specificResultsignatures. - Type Guards:
isResultfunction to check if a value is a Result type at runtime.
Installation
pnpm add t-resultBasic Usage
import { Result } from 't-result';
// Define a function that might fail
function divide(
numerator: number,
denominator: number,
): Result<number, string> {
if (denominator === 0) {
return Result.err('Cannot divide by zero!');
}
return Result.ok(numerator / denominator);
}
const result1 = divide(10, 2);
if (result1.ok) {
console.log('Result:', result1.value); // Output: Result: 5
} else {
console.error('Error:', result1.error);
}
const result2 = divide(10, 0);
if (result2.ok) {
console.log('Result:', result2.value);
} else {
console.error('Error:', result2.error); // Output: Error: Cannot divide by zero!
}
// Using chaining methods
const processedResult = divide(20, 2)
.mapOk((value) => value * 2) // Multiply if Ok
.mapErr((errorMsg) => `Computation failed: ${errorMsg}`);
if (processedResult.ok) {
console.log('Processed Value:', processedResult.value); // Output: Processed Value: 20
} else {
console.error('Processed Error:', processedResult.error);
}API
Result<T, E extends ResultValidErrors = Error>
A union type representing either a successful outcome (Ok<T>) or an error (Err<E>).
T: The type of the value in case of success.E: The type of the error in case of failure. Defaults toError.ResultValidErrorsincludesError,Record<string, unknown>,unknown[],readonly unknown[], ortrue.
An object of type Result<T, E> will have:
ok: boolean:trueif the result isOk<T>,falseifErr<E>.value: T: (Only ifokistrue) The successful value.error: E: (Only ifokisfalse) The error value.
Methods available on Result objects:
unwrapOrNull(): T | null: Returns the value ifOk, otherwisenull.unwrapOr<R extends T>(defaultValue: R): T | R: Returns the value ifOk, otherwisedefaultValue.unwrap(): T: Returns the value ifOk, otherwise throws the error (or anErrorwrapping the error if it's not anErrorinstance).mapOk<NewValue>(mapFn: (value: T) => NewValue): Result<NewValue, E>: IfOk, appliesmapFnto the value and returns a newOk<NewValue>. Otherwise, returns the originalErr<E>.mapErr<NewError extends ResultValidErrors>(mapFn: (error: E) => NewError): Result<T, NewError>: IfErr, appliesmapFnto the error and returns a newErr<NewError>. Otherwise, returns the originalOk<T>.mapOkAndErr<NewValue, NewError extends ResultValidErrors>(mapFns: { ok: (value: T) => NewValue; err: (error: E) => NewError; }): Result<NewValue, NewError>: Applies the appropriate mapping function based on whether the result isOkorErr.onOk(fn: (value: T) => void): Result<T, E>: IfOk, callsfnwith the value. Returns the originalResult.onErr(fn: (error: E) => void): Result<T, E>: IfErr, callsfnwith the error. Returns the originalResult.ifOk(fn: (value: T) => void): Result<T, E>: [DEPRECATED] UseonOkinstead. IfOk, callsfnwith the value. Returns the originalResult.ifErr(fn: (error: E) => void): Result<T, E>: [DEPRECATED] UseonErrinstead. IfErr, callsfnwith the error. Returns the originalResult.
ok<T>(value: T): Ok<T>
ok(): Ok<void>
Creates an Ok result, representing a successful operation.
value(optional): The value of the successful operation. If not provided, defaults toOk<void>.
err<E extends ResultValidErrors>(error: E): Err<E>
Creates an Err result, representing a failed operation.
error: The error value.
An Err<E> object also has an errorResult(): Err<E> method that returns a new Err result with the same error.
isResult(value: unknown): value is Result<any, ResultValidErrors>
Type guard to check if a value is a Result type.
value: The value to check- Returns:
trueif the value is aResult,falseotherwise
import { isResult } from 't-result';
if (isResult(someValue)) {
// someValue is now typed as Result<any, ResultValidErrors>
console.log(someValue.ok);
}Result Namespace/Object
A utility object with helper functions:
Result.ok: typeof ok: Reference to theokfunction.Result.err: typeof err: Reference to theerrfunction.Result.unknownToError(error: unknown): Err<Error>: Converts an unknown caught value into anErr<Error>. Internally usesunknownToError.Result.asyncUnwrap<T>(result: Promise<Result<T, ResultValidErrors>>): Promise<T>: Unwraps aPromisethat resolves to aResult. If theResultisOk, resolves with the value. IfErr, rejects with the error.Result.asyncMap<T, E extends ResultValidErrors>(resultPromise: Promise<Result<T, E>>): Provides methods to map over aPromise<Result<T,E>>:.ok<NewValue>(mapFn: (value: T) => NewValue): Promise<Result<NewValue, E>>.err<NewError extends ResultValidErrors>(mapFn: (error: E) => NewError): Promise<Result<T, NewError>>.okAndErr<NewValue, NewError extends ResultValidErrors>(mapFns: { ok: (value: T) => NewValue; err: (error: E) => NewError; }): Promise<Result<NewValue, NewError>>
Result.getOkErr: SeegetOkErrdocumentation below.Result.errId<E extends string>(id: E): Err<{ id: E }>: Creates anErrresult with an error object containing a unique id.
resultify
Converts a function call or a Promise into a Result or Promise<Result>.
Overloads:
resultify<T, E extends ResultValidErrors = Error>(fn: () => T extends Promise<any> ? never : T, errorNormalizer?: (err: unknown) => E): Result<T, E>- For synchronous functions that do not return a Promise.
resultify<T, E extends ResultValidErrors = Error>(fn: () => Promise<T>, errorNormalizer?: (err: unknown) => E): Promise<Result<Awaited<T>, E>>- For functions that return a Promise.
resultify<T, E extends ResultValidErrors = Error>(fn: Promise<T>, errorNormalizer?: (err: unknown) => E): Promise<Result<T, E>>- For existing Promises.
fn: The function to execute or the Promise to wrap.errorNormalizer(optional): A function to transform caught errors. If not provided,unknownToErroris used.
unknownToError(error: unknown): Error
Converts an unknown error value (e.g., from a catch block) into an Error object.
- If
erroris already anError, it's returned directly. - If
erroris a string, it becomes the message of a newError. - If
erroris an object, it attempts to useerror.messageor stringifies the object. - Other types are stringified.
- The original error is preserved as the
causeproperty of the returnedError.
getOkErr and TypedResult<T, E>
getOkErr is a utility to create typed ok and err factory functions for a specific Result signature.
type TypedResult<T, E extends ResultValidErrors = Error> = { ok: (value: T) => Ok<T>; err: (error: E) => Err<E>; _type: Result<T, E>; };
Overloads for getOkErr:
getOkErr<F extends (...args: any[]) => Promise<Result<any, any>>>(): TypedResult<Awaited<ReturnType<F>> extends Result<infer T, any> ? T : never, ...>- Infers types from an async function returning
Result.
- Infers types from an async function returning
getOkErr<F extends (...args: any[]) => Result<any, any>>>(): TypedResult<ReturnType<F> extends Result<infer T, any> ? T : never, ...>- Infers types from a sync function returning
Result.
- Infers types from a sync function returning
getOkErr<R extends Result<any, any>>(): TypedResult<R extends Result<infer T, any> ? T : never, ...>- Infers types from an existing
Resulttype.
- Infers types from an existing
getOkErr<T, E extends ResultValidErrors = Error>(): TypedResult<T, E>- Explicitly provide
TandE.
- Explicitly provide
The _type property on TypedResult can be used with typeof to get the Result<T,E> type (e.g., function foo(): typeof myTypedResult._type { ... }).
Examples
Creating and Handling Results
import { Result } from 't-result';
function parseNumber(input: string): Result<number, string> {
const num = parseFloat(input);
if (isNaN(num)) {
return Result.err(`'${input}' is not a valid number.`);
}
return Result.ok(num);
}
const validResult = parseNumber('123.45');
validResult
.onOk((value) => console.log(`Parsed: ${value}`)) // Parsed: 123.45
.onErr((error) => console.error(`Error: ${error}`));
const invalidResult = parseNumber('abc');
const valueOrDefault = invalidResult.unwrapOr(0);
console.log(`Value or default: ${valueOrDefault}`); // Value or default: 0
try {
const unwrappedValue = invalidResult.unwrap(); // This will throw
console.log(unwrappedValue);
} catch (e: any) {
console.error(`Caught error: ${e.message}`); // Caught error: 'abc' is not a valid number.
}Using resultify
import { resultify, unknownToError, Result } from 't-result';
// Synchronous function
function riskySyncOperation(shouldFail: boolean): string {
if (shouldFail) {
throw new Error('Sync operation failed!');
}
return 'Sync success!';
}
const syncRes = resultify(() => riskySyncOperation(false));
syncRes.onOk((val) => console.log(val)); // Sync success!
const failingSyncRes = resultify(() => riskySyncOperation(true));
failingSyncRes.onErr((err) => console.error(err.message)); // Sync operation failed!
// Asynchronous function / Promise
async function riskyAsyncOperation(shouldFail: boolean): Promise<string> {
if (shouldFail) {
throw new Error('Async operation failed!');
}
return 'Async success!';
}
async function testAsync() {
const asyncRes = await resultify(() => riskyAsyncOperation(false));
asyncRes.onOk((val) => console.log(val)); // Async success!
const failingAsyncRes = await resultify(riskyAsyncOperation(true));
failingAsyncRes.onErr((err) => console.error(err.message)); // Async operation failed!
// With custom error normalization
class CustomError extends Error {
constructor(
message: string,
public code: number,
) {
super(message);
}
}
const customErrorRes = await resultify(
() => riskyAsyncOperation(true),
(unknownErr) => {
const baseError = unknownToError(unknownErr);
return new CustomError(baseError.message, 500);
},
);
customErrorRes.onErr((customErr) =>
console.error(`${customErr.code}: ${customErr.message}`),
); // 500: Async operation failed!
}
testAsync();Using Result.safeFn
import { Result } from 't-result';
const divide = Result.safeFn((a: number, b: number) => {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
});
const result = divide(10, 0); // Result<number, Error>
// with custom error normalizer
const divide = Result.safeFn(
(a: number, b: number) => {
if (b === 0) throw new Error('Cannot divide by zero');
return a / b;
},
(err) => {
return {
message: err instanceof Error ? err.message : 'Unknown error',
};
},
);
const result = divide(10, 0); // Result<number, { message: string }>Using unknownToError
import { unknownToError } from 't-result';
try {
// Simulate an operation that might throw anything
throw { detail: 'Something went wrong', code: 101 };
} catch (caughtError) {
const error = unknownToError(caughtError);
console.error(`Error Message: ${error.message}`); // Error Message: {"detail":"Something went wrong","code":101}
if (error.cause) {
console.error(`Original Cause:`, error.cause); // Original Cause: { detail: 'Something went wrong', code: 101 }
}
}
try {
throw 'A simple string error';
} catch (caughtError) {
const error = unknownToError(caughtError);
console.error(`Error Message: ${error.message}`); // Error Message: A simple string error
}Using getOkErr for Typed Results
import { Result, getOkErr } from 't-result';
type User = { id: number; name: string };
type UserFetchError = { type: 'NotFound' | 'NetworkError'; message: string };
// Create typed ok/err factories
const UserResult = getOkErr<User, UserFetchError>();
function fetchUser(userId: number): typeof UserResult._type {
// Use _type for return type
if (userId <= 0) {
return UserResult.err({
type: 'NotFound',
message: 'User ID must be positive.',
});
}
if (Math.random() < 0.2) {
// Simulate network error
return UserResult.err({
type: 'NetworkError',
message: 'Failed to connect.',
});
}
return UserResult.ok({ id: userId, name: 'Jane Doe' });
}
const userResult = fetchUser(1);
userResult.mapOkAndErr({
ok: (user) => console.log(`Fetched user: ${user.name}`),
err: (error) => console.error(`Error (${error.type}): ${error.message}`),
});Using isResult Type Guard
import { isResult, Result } from 't-result';
function processUnknownValue(value: unknown) {
if (isResult(value)) {
// value is now typed as Result<any, ResultValidErrors>
if (value.ok) {
console.log('Success value:', value.value);
} else {
console.error('Error:', value.error);
}
} else {
console.log('Not a Result type:', value);
}
}
processUnknownValue(Result.ok('Hello')); // Success value: Hello
processUnknownValue(Result.err('Error occurred')); // Error: Error occurred
processUnknownValue('regular string'); // Not a Result type: regular stringUsing Result.errId
It's not possible to pass a string to Result.err because empty strings are falsey. To help overcome this, Result.errId can be used to create an Err result with an error object containing a unique id.
import { Result } from 't-result';
function foo(): Result<number, { id: 'lessThan0.5' }> {
if (Math.random() < 0.5) {
return Result.errId('lessThan0.5');
}
return Result.ok(1);
}
const err = foo();