Mastering Error Handling with Container Types in TypeScript

The Struggle is Real: Dealing with Null and Undefined

We’ve all been there – stuck in a never-ending battle with null and undefined values in our code. It’s a problem that’s as old as computer science itself.

The Concept of Emptiness

When we annotate a variable with a type, it can hold either a value of that type, null, or undefined. This means we must consistently apply defensive programming techniques to avoid errors like “Cannot read property ‘XYZ’ of undefined.” But even with defensive programming, things can go wrong. False negatives can occur, and APIs often use null and undefined inconsistently.

Introducing the Maybe Data Type

Wouldn’t it be nice if we could consistently handle emptiness without false negatives? That’s where the Maybe data type comes in. Also known as Option, Maybe encapsulates the idea of a value that might not be there. A Maybe value can either be Just some value or Nothing.

Using Maybe in Practice

Let’s take a look at how we can use Maybe in a real-world scenario. We’ll create a User parser for our API result, using a UserJson type to represent what we get from the server and a User type to represent our domain model.


interface UserJson {
  id: number;
  name: string;
}

interface User {
  id: number;
  name: string;
}

class Maybe {
  private value: T | null;

  constructor(value: T | null) {
    this.value = value;
  }

  static of(value: T): Maybe {
    return new Maybe(value);
  }

  static nothing(): Maybe {
    return new Maybe(null);
  }

  map(fn: (value: T) => U): Maybe {
    return this.value? Maybe.of(fn(this.value)) : Maybe.nothing();
  }
}

Extracting Values from Maybe

Now that we’ve created Maybe instances, let’s see how we can use them in our logic. We’ll create an HTML representation of the list of users from the server, representing them with cards.


function getUsersFromApi(): Maybe<userjson[]> {
  // API call to retrieve users
}

function renderUserCards(users: Maybe<userjson[]>): string {
  return users.map(userList => userList.map(user => `<div>${user.name}</div>`).join('')).getOrElse('');
}
</userjson[]></userjson[]>

Maybe-fying Existing APIs

To complete our application, we need to render our user cards. We’ll create a Maybe-fied version of the getElementById function to avoid dealing with null.


function getElementById(id: string): Maybe {
  return Maybe.of(document.getElementById(id));
}

The Either Data Type: Handling Errors with Elegance

Errors are an essential part of software development. Defining failure consistently is vital to making our programs easier to reason about. The Either data type abstracts away if and try-catch statements, reducing the number of intermediate variables needed to transport the final result.

Using Either in Practice

Let’s create a getUserById service that searches for a user by ID from a JSON file. We’ll use Either to keep errors under control.


interface Error {
  message: string;
}

class Either<t, e="" extends="" error=""> {
  private value: T | E;

  constructor(value: T | E) {
    this.value = value;
  }

  static left(error: E): Either<never, e=""> {
    return new Either(error);
  }

  static right(value: T): Either<t, never=""> {
    return new Either(value);
  }

  map(fn: (value: T) => U): Either<u, e=""> {
    return this.value instanceof Error? Either.left(this.value) : Either.right(fn(this.value));
  }
}

function getUserById(id: number): Either<userjson, error=""> {
  // JSON file search logic
}
</userjson,></u,></t,></never,></t,>

Either Composition

Our getUserById function is a sequence of actions where the next depends on the outcome of the previous. Each step does something that may fail and passes the result to the next one.


function getUserById(id: number): Either<userjson, error=""> {
  return getUserFromJsonFile(id)
   .map(parseUserJson)
   .map(validateUser);
}
</userjson,>

Using Our getUserById Service

Our service returns a UserJson buried two levels deep, one in an Either and the other in a Maybe. Let’s extract this valuable information from our container types.


function renderUserCard(user: Either<userjson, error="">): string {
  return user.map(userJson => `<div>${userJson.name}</div>`).getOrElse('Error: User not found');
}
</userjson,>

Leave a Reply