wellcrafted
Version:
Delightful TypeScript patterns for elegant, type-safe applications
333 lines (332 loc) • 13 kB
JavaScript
//#region src/result/result.ts
/**
* Constructs an `Ok<T>` variant, representing a successful outcome.
*
* This factory function creates the success variant of a `Result`.
* It wraps the provided `data` (the success value) and ensures the `error` property is `null`.
*
* @template T - The type of the success value.
* @param data - The success value to be wrapped in the `Ok` variant.
* @returns An `Ok<T>` object with the provided data and `error` set to `null`.
* @example
* ```ts
* const successfulResult = Ok("Operation completed successfully");
* // successfulResult is { data: "Operation completed successfully", error: null }
* ```
*/
const Ok = (data) => ({
data,
error: null
});
/**
* Constructs an `Err<E>` variant, representing a failure outcome.
*
* This factory function creates the error variant of a `Result`.
* It wraps the provided `error` (the error value) and ensures the `data` property is `null`.
*
* @template E - The type of the error value.
* @param error - The error value to be wrapped in the `Err` variant. This value represents the specific error that occurred.
* @returns An `Err<E>` object with the provided error and `data` set to `null`.
* @example
* ```ts
* const failedResult = Err(new TypeError("Invalid input"));
* // failedResult is { error: TypeError("Invalid input"), data: null }
* ```
*/
const Err = (error) => ({
error,
data: null
});
/**
* Type guard to runtime check if an unknown value is a valid `Result<T, E>`.
*
* A value is considered a valid `Result` if:
* 1. It is a non-null object.
* 2. It has both `data` and `error` properties.
* 3. Exactly one of `data` or `error` is `null`. The other must be non-`null`.
*
* This function does not validate the types of `data` or `error` beyond `null` checks.
*
* @template T - The expected type of the success value if the value is an `Ok` variant (defaults to `unknown`).
* @template E - The expected type of the error value if the value is an `Err` variant (defaults to `unknown`).
* @param value - The value to check.
* @returns `true` if the value conforms to the `Result` structure, `false` otherwise.
* If `true`, TypeScript's type system will narrow `value` to `Result<T, E>`.
* @example
* ```ts
* declare const someValue: unknown;
*
* if (isResult<string, Error>(someValue)) {
* // someValue is now typed as Result<string, Error>
* if (isOk(someValue)) {
* console.log(someValue.data); // string
* } else {
* console.error(someValue.error); // Error
* }
* }
* ```
*/
function isResult(value) {
const isNonNullObject = typeof value === "object" && value !== null;
if (!isNonNullObject) return false;
const hasDataProperty = "data" in value;
const hasErrorProperty = "error" in value;
if (!hasDataProperty || !hasErrorProperty) return false;
const isBothNull = value.data === null && value.error === null;
if (isBothNull) return false;
const isNeitherNull = value.data !== null && value.error !== null;
if (isNeitherNull) return false;
return true;
}
/**
* Type guard to runtime check if a `Result<T, E>` is an `Ok<T>` variant.
*
* This function narrows the type of a `Result` to `Ok<T>` if it represents a successful outcome.
* An `Ok<T>` variant is identified by its `error` property being `null`.
*
* @template T - The success value type.
* @template E - The error value type.
* @param result - The `Result<T, E>` to check.
* @returns `true` if the `result` is an `Ok<T>` variant, `false` otherwise.
* If `true`, TypeScript's type system will narrow `result` to `Ok<T>`.
* @example
* ```ts
* declare const myResult: Result<number, string>;
*
* if (isOk(myResult)) {
* // myResult is now typed as Ok<number>
* console.log("Success value:", myResult.data); // myResult.data is number
* }
* ```
*/
function isOk(result) {
return result.error === null;
}
/**
* Type guard to runtime check if a `Result<T, E>` is an `Err<E>` variant.
*
* This function narrows the type of a `Result` to `Err<E>` if it represents a failure outcome.
* An `Err<E>` variant is identified by its `error` property being non-`null` (and thus `data` being `null`).
*
* @template T - The success value type.
* @template E - The error value type.
* @param result - The `Result<T, E>` to check.
* @returns `true` if the `result` is an `Err<E>` variant, `false` otherwise.
* If `true`, TypeScript's type system will narrow `result` to `Err<E>`.
* @example
* ```ts
* declare const myResult: Result<number, string>;
*
* if (isErr(myResult)) {
* // myResult is now typed as Err<string>
* console.error("Error value:", myResult.error); // myResult.error is string
* }
* ```
*/
function isErr(result) {
return result.error !== null;
}
/**
* Executes a synchronous operation and wraps its outcome (success or failure) in a `Result<T, E>`.
*
* This function attempts to execute the `operation`.
* - If `operation` completes successfully, its return value is wrapped in an `Ok<T>` variant.
* - If `operation` throws an exception, the caught exception (of type `unknown`) is passed to
* the `mapErr` function. `mapErr` is responsible for transforming this `unknown`
* exception into an `Err<E>` variant containing a well-typed error value of type `E`.
*
* @template T - The type of the success value returned by the `operation` if it succeeds.
* @template E - The type of the error value produced by `mapErr` if the `operation` fails.
* @param options - An object containing the operation and error mapping function.
* @param options.try - The synchronous operation to execute. This function is expected to return a value of type `T`.
* @param options.mapErr - A function that takes the `unknown` exception caught from `options.try`
* and transforms it into an `Err<E>` variant containing a specific error value of type `E`.
* @returns A `Result<T, E>`: `Ok<T>` if `options.try` succeeds, or `Err<E>` if it throws and `options.mapErr` provides an error variant.
* @example
* ```ts
* function parseJson(jsonString: string): Result<object, SyntaxError> {
* return trySync({
* try: () => JSON.parse(jsonString),
* mapErr: (err: unknown) => {
* if (err instanceof SyntaxError) return Err(err);
* return Err(new SyntaxError("Unknown parsing error"));
* }
* });
* }
*
* const validResult = parseJson('{"name":"Result"}'); // Ok<{name: string}>
* const invalidResult = parseJson('invalid json'); // Err<SyntaxError>
*
* if (isOk(validResult)) console.log(validResult.data);
* if (isErr(invalidResult)) console.error(invalidResult.error.message);
* ```
*/
function trySync({ try: operation, mapErr }) {
try {
const data = operation();
return Ok(data);
} catch (error) {
return mapErr(error);
}
}
/**
* Executes an asynchronous operation (returning a `Promise`) and wraps its outcome in a `Promise<Result<T, E>>`.
*
* This function attempts to execute the asynchronous `operation`.
* - If the `Promise` returned by `operation` resolves successfully, its resolved value is wrapped in an `Ok<T>` variant.
* - If the `Promise` returned by `operation` rejects, or if `operation` itself throws an exception synchronously,
* the caught exception/rejection reason (of type `unknown`) is passed to the `mapErr` function.
* `mapErr` is responsible for transforming this `unknown` error into an `Err<E>` variant containing
* a well-typed error value of type `E`.
*
* The entire outcome (`Ok<T>` or `Err<E>`) is wrapped in a `Promise`.
*
* @template T - The type of the success value the `Promise` from `operation` resolves to.
* @template E - The type of the error value produced by `mapErr` if the `operation` fails or rejects.
* @param options - An object containing the asynchronous operation and error mapping function.
* @param options.try - The asynchronous operation to execute. This function must return a `Promise<T>`.
* @param options.mapErr - A function that takes the `unknown` exception/rejection reason caught from `options.try`
* and transforms it into an `Err<E>` variant containing a specific error value of type `E`.
* This function must return `Err<E>` directly.
* @returns A `Promise` that resolves to a `Result<T, E>`: `Ok<T>` if `options.try`'s `Promise` resolves,
* or `Err<E>` if it rejects/throws and `options.mapErr` provides an error variant.
* @example
* ```ts
* async function fetchData(url: string): Promise<Result<Response, Error>> {
* return tryAsync({
* try: async () => fetch(url),
* mapErr: (err: unknown) => {
* if (err instanceof Error) return Err(err);
* return Err(new Error("Network request failed"));
* }
* });
* }
*
* async function processData() {
* const result = await fetchData("/api/data");
* if (isOk(result)) {
* const response = result.data;
* console.log("Data fetched:", await response.json());
* } else {
* console.error("Fetch error:", result.error.message);
* }
* }
* processData();
* ```
*/
async function tryAsync({ try: operation, mapErr }) {
try {
const data = await operation();
return Ok(data);
} catch (error) {
return mapErr(error);
}
}
/**
* Resolves a value that may or may not be wrapped in a `Result`, returning the final value.
*
* This function handles the common pattern where a value might be a `Result<T, E>` or a plain `T`:
* - If `value` is an `Ok<T>` variant, returns the contained success value.
* - If `value` is an `Err<E>` variant, throws the contained error value.
* - If `value` is not a `Result` (i.e., it's already a plain value of type `T`),
* returns it as-is.
*
* This is useful when working with APIs that might return either direct values or Results,
* allowing you to normalize them to the actual value or propagate errors via throwing.
*
* Use `resolve` when the input might or might not be a Result.
* Use `unwrap` when you know the input is definitely a Result.
*
* @template T - The type of the success value (if `value` is `Ok<T>`) or the type of the plain value.
* @template E - The type of the error value (if `value` is `Err<E>`).
* @param value - The value to resolve. Can be a `Result<T, E>` or a plain value of type `T`.
* @returns The final value of type `T` if `value` is `Ok<T>` or if `value` is already a plain `T`.
* @throws The error value `E` if `value` is an `Err<E>` variant.
*
* @example
* ```ts
* // Example with an Ok variant
* const okResult = Ok("success data");
* const resolved = resolve(okResult); // "success data"
*
* // Example with an Err variant
* const errResult = Err(new Error("failure"));
* try {
* resolve(errResult);
* } catch (e) {
* console.error(e.message); // "failure"
* }
*
* // Example with a plain value
* const plainValue = "plain data";
* const resolved = resolve(plainValue); // "plain data"
*
* // Example with a function that might return Result or plain value
* declare function mightReturnResult(): string | Result<string, Error>;
* const outcome = mightReturnResult();
* try {
* const finalValue = resolve(outcome); // handles both cases
* console.log("Final value:", finalValue);
* } catch (e) {
* console.error("Operation failed:", e);
* }
* ```
*/
/**
* Unwraps a `Result<T, E>`, returning the success value or throwing the error.
*
* This function extracts the data from a `Result`:
* - If the `Result` is an `Ok<T>` variant, returns the contained success value of type `T`.
* - If the `Result` is an `Err<E>` variant, throws the contained error value of type `E`.
*
* Unlike `resolve`, this function expects the input to always be a `Result` type,
* making it more direct for cases where you know you're working with a `Result`.
*
* @template T - The type of the success value contained in the `Ok<T>` variant.
* @template E - The type of the error value contained in the `Err<E>` variant.
* @param result - The `Result<T, E>` to unwrap.
* @returns The success value of type `T` if the `Result` is `Ok<T>`.
* @throws The error value of type `E` if the `Result` is `Err<E>`.
*
* @example
* ```ts
* // Example with an Ok variant
* const okResult = Ok("success data");
* const value = unwrap(okResult); // "success data"
*
* // Example with an Err variant
* const errResult = Err(new Error("something went wrong"));
* try {
* unwrap(errResult);
* } catch (error) {
* console.error(error.message); // "something went wrong"
* }
*
* // Usage in a function that returns Result
* function divide(a: number, b: number): Result<number, string> {
* if (b === 0) return Err("Division by zero");
* return Ok(a / b);
* }
*
* try {
* const result = unwrap(divide(10, 2)); // 5
* console.log("Result:", result);
* } catch (error) {
* console.error("Division failed:", error);
* }
* ```
*/
function unwrap(result) {
if (isOk(result)) return result.data;
throw result.error;
}
function resolve(value) {
if (isResult(value)) {
if (isOk(value)) return value.data;
throw value.error;
}
return value;
}
//#endregion
export { Err, Ok, isErr, isOk, isResult, resolve, tryAsync, trySync, unwrap };
//# sourceMappingURL=result-BJtW-Wuc.js.map