#Result Type
Before diving into the API, let's understand what the Result type is and why it's useful.
#What is a Result?
A Result is a type that represents the outcome of an operation that might fail.
It can be one of two variants:
- Success: The operation succeeded and contains a value
- Failure: The operation failed and contains an error
This is fundamentally different from throwing exceptions. With Result, the possibility of failure is encoded in the type system itself.
#The Structure
A Result is a simple object with a type discriminator:
import { import Result Result } from '@praha/byethrow';
// A Success result
const const success: Result.Success<number> success : import Result Result .type Success<T> = {
readonly type: "Success";
readonly value: T;
}
Represents a successful result.
@typeParamT - The type of the successful value.@exampleimport { Result } from '@praha/byethrow';
const success: Result.Success<number> = {
type: 'Success',
value: 42,
};
@categoryCore Types Success <number> = {
type: "Success" type : 'Success',
value: number value : 42,
};
// A Failure result
const const failure: Result.Failure<string> failure : import Result Result .type Failure<E> = {
readonly type: "Failure";
readonly error: E;
}
Represents a failed result.
@typeParamE - The type of the error.@exampleimport { Result } from '@praha/byethrow';
const failure: Result.Failure<string> = {
type: 'Failure',
error: 'Something went wrong',
};
@categoryCore Types Failure <string> = {
type: "Failure" type : 'Failure',
error: string error : 'Something went wrong',
};#The Union Type
The Result.Result<T, E> type is a union of Success<T> and Failure<E>:
import { import Result Result } from '@praha/byethrow';
// This function returns either a Success<number> or a Failure<string>
const const divide: (a: number, b: number) => Result.Result<number, string> divide = (a: number a : number, b: number b : number): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <number, string> => {
if (b: number b === 0) {
return { type: "Failure" type : 'Failure', error: string error : 'Cannot divide by zero' };
}
return { type: "Success" type : 'Success', value: number value : a: number a / b: number b };
};
const const result: Result.Result<number, string> result = const divide: (a: number, b: number) => Result.Result<number, string> divide (10, 2);
// Type: Result.Result<number, string>#Why Use Result Instead of Exceptions?
#1. Explicit Error Handling
With exceptions, you never know if a function might throw:
// ❌ Does this throw? We can't tell from the signature
const const parseConfig: (path: string) => Config parseConfig = (path: string path : string): type Config = {
host: string;
port: number;
}
Config => {
// ...
}With Result, it's clear:
// ✅ The return type tells us this might fail
const const parseConfig: (path: string) => Result.Result<Config, ParseError> parseConfig = (path: string path : string): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <type Config = {
host: string;
port: number;
}
Config , class ParseError ParseError > => {
// ...
}#2. Type-Safe Errors
Exceptions lose type information. Result preserves it:
import { import Result Result } from '@praha/byethrow';
type type ValidationError = {
field: string;
message: string;
}
ValidationError = { field: string field : string; message: string message : string };
const const validateEmail: (email: string) => Result.Result<string, ValidationError> validateEmail = (email: string email : string): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <string, type ValidationError = {
field: string;
message: string;
}
ValidationError > => {
if (!email: string email .String.includes(searchString: string, position?: number): booleanReturns true if searchString appears as a substring of the result of converting this
object to a String, at one or more positions that are
greater than or equal to position; otherwise, returns false.
@paramsearchString search string@paramposition If position is undefined, 0 is assumed, so as to search all of the String. includes ('@')) {
return import Result Result .const fail: <{
readonly field: "email";
readonly message: "Invalid email format";
}>(error: {
readonly field: "email";
readonly message: "Invalid email format";
}) => Result.Result<never, {
readonly field: "email";
readonly message: "Invalid email format";
}> (+1 overload)
fail ({ field: "email" field : 'email', message: "Invalid email format" message : 'Invalid email format' });
}
return import Result Result .const succeed: <string>(value: string) => Result.Result<string, never> (+1 overload) succeed (email: string email );
};
const const result: Result.Result<string, ValidationError> result = const validateEmail: (email: string) => Result.Result<string, ValidationError> validateEmail ('test');
if (import Result Result .const isFailure: <Result.Result<string, ValidationError>>(result: Result.Result<string, ValidationError>) => result is Result.Failure<ValidationError>Type guard to check if a
Result
is a
Failure
.
@function@typeParamR - The type of the result to check.@paramresult - The Result to check.@returnstrue if the result is a Failure , otherwise false.@exampleimport { Result } from '@praha/byethrow';
const result: Result.Result<number, string> = { type: 'Failure', error: 'Something went wrong' };
if (Result.isFailure(result)) {
console.error(result.error); // Safe access to error
}
@categoryType Guards isFailure (const result: Result.Result<string, ValidationError> result )) {
// TypeScript knows result.error is ValidationError
var console: Console console .Console.log(...data: any[]): voidThe console.log() static method outputs a message to the console.
log (`Error in ${const result: Result.Failure<ValidationError> result .error: ValidationError error .field: string field }: ${const result: Result.Failure<ValidationError> result .error: ValidationError error .message: string message }`);
}#3. Composable
Results can be easily chained and composed (we'll cover this in later sections):
import { import Result Result } from '@praha/byethrow';
const const result: Result.ResultAsync<User, ValidationError | TransformError | SaveError> result = import Result Result .const pipe: <Result.Result<{
id: string;
name: string;
}, never>, Result.Result<User, ValidationError>, Result.Result<User, ValidationError | TransformError>, Result.ResultAsync<User, ValidationError | TransformError | SaveError>>(a: Result.Result<{
id: string;
name: string;
}, never>, ab: (a: Result.Result<{
id: string;
name: string;
}, never>) => Result.Result<User, ValidationError>, bc: (b: Result.Result<...>) => Result.Result<...>, cd: (c: Result.Result<...>) => Result.ResultAsync<...>) => Result.ResultAsync<...> (+25 overloads)
pipe (
import Result Result .const succeed: <{
id: string;
name: string;
}>(value: {
id: string;
name: string;
}) => Result.Result<{
id: string;
name: string;
}, never> (+1 overload)
succeed (const input: {
id: string;
name: string;
}
input ),
import Result Result .const andThen: <Result.Result<{
id: string;
name: string;
}, never>, Result.Result<User, ValidationError>>(fn: (a: {
id: string;
name: string;
}) => Result.Result<User, ValidationError>) => (result: Result.Result<{
id: string;
name: string;
}, never>) => Result.Result<User, ValidationError> (+1 overload)
andThen (const validate: (value: User) => Result.Result<User, ValidationError> validate ),
import Result Result .const andThen: <Result.Result<User, ValidationError>, Result.Result<User, TransformError>>(fn: (a: User) => Result.Result<User, TransformError>) => (result: Result.Result<User, ValidationError>) => Result.Result<User, ValidationError | TransformError> (+1 overload) andThen (const transform: (value: User) => Result.Result<User, TransformError> transform ),
import Result Result .const andThen: <Result.Result<User, ValidationError | TransformError>, Result.ResultAsync<User, SaveError>>(fn: (a: User) => Result.ResultAsync<User, SaveError>) => (result: Result.Result<User, ValidationError | TransformError>) => Result.ResultAsync<User, ValidationError | TransformError | SaveError> (+1 overload) andThen (const save: (value: User) => Result.ResultAsync<User, SaveError> save ),
);
// Type: Result.ResultAsync<User, ValidationError | TransformError | SaveError>#Async Results
@praha/byethrow also supports asynchronous operations.
Asynchronous Result is a type alias ResultAsync<T, E>, representing Promise<Result<T, E>>:
import { import Result Result } from '@praha/byethrow';
// ResultAsync is just Promise<Result<T, E>>
type type ResultAsync<T, E> = Promise<Result.Result<T, E>> ResultAsync <function (type parameter) T in type ResultAsync<T, E> T , function (type parameter) E in type ResultAsync<T, E> E > = interface Promise<T>Represents the completion of an asynchronous operation
Promise <import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <function (type parameter) T in type ResultAsync<T, E> T , function (type parameter) E in type ResultAsync<T, E> E >>;
// The library handles both sync and async seamlessly
const const asyncResult: Result.ResultAsync<number, never> asyncResult = import Result Result .const succeed: <Promise<number>>(value: Promise<number>) => Result.ResultAsync<number, never> (+1 overload) succeed (var Promise: PromiseConstructorRepresents the completion of an asynchronous operation
Promise .PromiseConstructor.resolve<number>(value: number): Promise<number> (+2 overloads)Creates a new resolved promise for the provided value.
@paramvalue A promise.@returnsA promise whose internal state matches the provided promise. resolve (42));
// Type: Result.ResultAsync<number, never>
const const resolved: Result.Result<number, never> resolved = await const asyncResult: Result.ResultAsync<number, never> asyncResult ;
// Type: Result.Result<number, never>#Seamless Sync/Async Chaining
One of the powerful features of @praha/byethrow is that you can seamlessly chain synchronous and asynchronous Results together. When you mix sync and async operations in a pipeline, the result automatically becomes a ResultAsync:
import { import Result Result } from '@praha/byethrow';
// Sync function
const const validate: (input: string) => Result.Result<string, Error> validate = (input: string input : string): import Result Result .type Result<T, E> = Result.Success<T> | Result.Failure<E>A union type representing either a success or a failure.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const doSomething = (): Result.Result<number, string> => {
return Math.random() > 0.5
? { type: 'Success', value: 10 }
: { type: 'Failure', error: 'Oops' };
};
@categoryCore Types Result <string, Error> => {
if (input: string input .String.length: numberReturns the length of a String object.
length === 0) {
return import Result Result .const fail: <Error>(error: Error) => Result.Result<never, Error> (+1 overload) fail (new var Error: ErrorConstructor
new (message?: string, options?: ErrorOptions) => Error (+1 overload)
Error ('Input is empty'));
}
return import Result Result .const succeed: <string>(value: string) => Result.Result<string, never> (+1 overload) succeed (input: string input );
};
// Async function
const const fetchData: (input: string) => Result.ResultAsync<number, Error> fetchData = async (input: string input : string): import Result Result .type ResultAsync<T, E> = Promise<Result.Result<T, E>>An asynchronous variant of
Result
, wrapped in a Promise.
@typeParamT - The type of the Success value.@typeParamE - The type of the Failure value.@exampleimport { Result } from '@praha/byethrow';
const fetchData = async (): Result.ResultAsync<string, Error> => {
try {
const data = await fetch('...');
return { type: 'Success', value: await data.text() };
} catch (err) {
return { type: 'Failure', error: err as Error };
}
};
@categoryCore Types ResultAsync <number, Error> => {
// Simulating an API call
return import Result Result .const succeed: <number>(value: number) => Result.Result<number, never> (+1 overload) succeed (input: string input .String.length: numberReturns the length of a String object.
length );
};
// Sync and async can be chained together seamlessly
const const result: Result.ResultAsync<number, Error> result = import Result Result .const pipe: <Result.Result<"hello", never>, Result.Result<string, Error>, Result.ResultAsync<number, Error>, Result.ResultAsync<number, Error>>(a: Result.Result<"hello", never>, ab: (a: Result.Result<"hello", never>) => Result.Result<string, Error>, bc: (b: Result.Result<string, Error>) => Result.ResultAsync<number, Error>, cd: (c: Result.ResultAsync<number, Error>) => Result.ResultAsync<number, Error>) => Result.ResultAsync<...> (+25 overloads) pipe (
import Result Result .const succeed: <"hello">(value: "hello") => Result.Result<"hello", never> (+1 overload) succeed ('hello'),
import Result Result .const andThen: <Result.Result<"hello", never>, Result.Result<string, Error>>(fn: (a: "hello") => Result.Result<string, Error>) => (result: Result.Result<"hello", never>) => Result.Result<string, Error> (+1 overload) andThen (const validate: (input: string) => Result.Result<string, Error> validate ), // sync
import Result Result .const andThen: <Result.Result<string, Error>, Result.ResultAsync<number, Error>>(fn: (a: string) => Result.ResultAsync<number, Error>) => (result: Result.Result<string, Error>) => Result.ResultAsync<number, Error> (+1 overload) andThen (const fetchData: (input: string) => Result.ResultAsync<number, Error> fetchData ), // async - from here, the pipeline becomes async
import Result Result .const andThen: <Result.ResultAsync<number, Error>, Result.Result<number, never>>(fn: (a: number) => Result.Result<number, never>) => (result: Result.ResultAsync<number, Error>) => Result.ResultAsync<number, Error> (+1 overload) andThen ((n: number n ) => import Result Result .const succeed: <number>(value: number) => Result.Result<number, never> (+1 overload) succeed (n: number n * 2)), // sync, but still in async context
);
// Type: Result.ResultAsync<number, Error>#References
| Function | Purpose |
|---|---|
| Success<T> | Represents a successful result |
| Failure<T> | Represents a failed result |
| Result<T, E> | A union type of Success or Failure |
| ResultAsync<T, E> | An asynchronous variant of Result |
