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:
- Generic Types: The
Eithertype should be generic over two types:Lfor the left-hand side andRfor the right-hand side. - Discriminator: The
Eithertype 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. - Construction: You need to provide factory functions to create
Eitherinstances:left<L, R>(value: L): Either<L, R>: Creates anEitherholding a left value.right<L, R>(value: R): Either<L, R>: Creates anEitherholding a right value.
- Methods for Handling: Implement methods to process the
Eithervalue:isLeft(): boolean: Returnstrueif theEitherholds a left value,falseotherwise.isRight(): boolean: Returnstrueif theEitherholds a right value,falseotherwise.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 typeTby applying the appropriate function based on theEither's state.map<R2>(f: (r: R) => R2): Either<L, R2>: If theEitheris a right value, apply the functionfto the right value and return a newEitherwith the transformed right value. If it's a left value, return the original leftEitherunchanged.flatMap<R2>(f: (r: R) => Either<L, R2>): Either<L, R2>: Similar tomap, but the functionfitself returns anEither. This is useful for chaining operations that can fail. If theEitheris a right value, applyfto the right value and return the resultingEither. If it's a left value, return the original leftEitherunchanged.
Expected Behavior:
- An
Eitherinstance should always contain either a left value or a right value, but not both, and not neither. - Methods like
isLeftandisRightshould accurately reflect the contained value. foldshould correctly execute the provided callbacks based on the type of value held.mapshould transform the right value only and preserve the left value.flatMapshould allow chaining operations that produceEithers.
Edge Cases:
- Consider how your implementation handles
nullorundefinedvalues 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
mapandflatMapwhen 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
Eithertype itself. - The type should be generic and work with any
LandRtypes. - 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, andflatMap. - The
foldmethod is often the most fundamental operation for extracting values from anEither. mapis analogous toArray.prototype.mapfor successful values.flatMapis analogous toArray.prototype.flatMapand is essential for composing functions that returnEither.