Hone logo
Hone
Problems

Implementing the Either Type in TypeScript

The Either type is a powerful functional programming construct that represents a value that can be one of two possible types. This is incredibly useful for handling operations that might either succeed with a result or fail with an error, without resorting to exceptions or nullable types. Your challenge is to implement a generic Either type in TypeScript that encapsulates this pattern.

Problem Description

You are tasked with creating a generic Either<L, R> type in TypeScript. This type should be able to hold either a value of type L (typically representing a left-hand side, often an error) or a value of type R (typically representing a right-hand side, often a success result).

Key Requirements:

  1. Generic Types: The Either type should be generic over two types: L for the left-hand side and R for the right-hand side.
  2. Discriminator: The Either type must clearly distinguish whether it currently holds a left value or a right value. A common approach is to use a boolean flag or a string literal union.
  3. Construction: You need to provide factory functions to create Either instances:
    • left<L, R>(value: L): Either<L, R>: Creates an Either holding a left value.
    • right<L, R>(value: R): Either<L, R>: Creates an Either holding a right value.
  4. Methods for Handling: Implement methods to process the Either value:
    • isLeft(): boolean: Returns true if the Either holds a left value, false otherwise.
    • isRight(): boolean: Returns true if the Either holds a right value, false otherwise.
    • fold<T>(onLeft: (l: L) => T, onRight: (r: R) => T): T: This is the core pattern matching method. It takes two functions, one for handling the left case and one for the right case, and returns a value of type T by applying the appropriate function based on the Either's state.
    • map<R2>(f: (r: R) => R2): Either<L, R2>: If the Either is a right value, apply the function f to the right value and return a new Either with the transformed right value. If it's a left value, return the original left Either unchanged.
    • flatMap<R2>(f: (r: R) => Either<L, R2>): Either<L, R2>: Similar to map, but the function f itself returns an Either. This is useful for chaining operations that can fail. If the Either is a right value, apply f to the right value and return the resulting Either. If it's a left value, return the original left Either unchanged.

Expected Behavior:

  • An Either instance should always contain either a left value or a right value, but not both, and not neither.
  • Methods like isLeft and isRight should accurately reflect the contained value.
  • fold should correctly execute the provided callbacks based on the type of value held.
  • map should transform the right value only and preserve the left value.
  • flatMap should allow chaining operations that produce Eithers.

Edge Cases:

  • Consider how your implementation handles null or undefined values passed to the factory functions (though a robust implementation might disallow them if they are not intended as valid error/success payloads).
  • The behavior of map and flatMap when encountering a left value is crucial.

Examples

Example 1: Basic Usage with fold

// Assume Either, left, right, and fold are implemented

const success: Either<string, number> = right(10);
const failure: Either<string, number> = left("Something went wrong");

const successMessage = success.fold(
  (error) => `Error: ${error}`,
  (value) => `Success: ${value}`
);

const failureMessage = failure.fold(
  (error) => `Error: ${error}`,
  (value) => `Success: ${value}`
);

// successMessage should be "Success: 10"
// failureMessage should be "Error: Something went wrong"

Example 2: Using map

// Assume Either, left, right, and map are implemented

const value: Either<string, number> = right(5);
const doubledValue = value.map(x => x * 2);

const errorValue: Either<string, number> = left("Could not get value");
const doubledError = errorValue.map(x => x * 2);

// doubledValue should be a right Either containing 10
// doubledError should be a left Either containing "Could not get value"

Example 3: Using flatMap for Chaining Operations

// Assume Either, left, right, and flatMap are implemented

const parseInteger = (str: string): Either<string, number> => {
  const num = parseInt(str, 10);
  return isNaN(num) ? left(`Invalid integer: ${str}`) : right(num);
};

const stringNumber: Either<string, string> = right("123");
const parsedNumber: Either<string, number> = stringNumber.flatMap(parseInteger);

const invalidStringNumber: Either<string, string> = right("abc");
const parsedInvalid: Either<string, number> = invalidStringNumber.flatMap(parseInteger);

const alreadyError: Either<string, string> = left("Initial error");
const flatMappedError: Either<string, number> = alreadyError.flatMap(parseInteger);

// parsedNumber should be a right Either containing 123
// parsedInvalid should be a left Either containing "Invalid integer: abc"
// flatMappedError should be a left Either containing "Initial error"

Constraints

  • The implementation must be purely in TypeScript.
  • Avoid using any external libraries for the Either type itself.
  • The type should be generic and work with any L and R types.
  • Performance is not a primary concern, but the implementation should be reasonably efficient.

Notes

  • Consider how you will represent the internal state of the Either (e.g., using a union type with a discriminator property).
  • Think about how to ensure type safety across all methods, especially fold, map, and flatMap.
  • The fold method is often the most fundamental operation for extracting values from an Either.
  • map is analogous to Array.prototype.map for successful values.
  • flatMap is analogous to Array.prototype.flatMap and is essential for composing functions that return Either.
Loading editor...
typescript