Functions

Maybe

Type-safe optional values without null/undefined

The Maybe type is a type-safe way to handle optional values without using null or undefined. It represents either a value present (Some) or no value (None).

Why Use Maybe?

JavaScript uses null and undefined to represent missing values, but this approach has several problems:

  • Runtime errors: Accessing properties on null/undefined throws errors
  • Type safety: TypeScript's null and undefined are easy to miss
  • Ambiguity: It's often unclear whether a value can be missing
  • Falsy values: 0, "", and false are often confused with missing values

Maybe solves these problems by making optional values explicit in the type system.

Basic Usage

Creating Maybe Values

Use the Maybe namespace constructors:

import { Maybe } from '@deessejs/functions';

// Some - value exists
const maybe1 = Maybe.some(42);
console.log(maybe1.value); // 42

// None - no value
const maybe2 = Maybe.none();
// No value to access

Type Guards

Use isSome() and isNone() to check and narrow types:

function findUser(id: number): Maybe<User> {
  const user = database.find(id);

  if (!user) {
    return Maybe.none();
  }

  return Maybe.some(user);
}

const user = findUser(123);

if (user.isSome()) {
  // TypeScript knows user is Some here
  console.log("Found:", user.value.name);
} else {
  // TypeScript knows user is None here
  console.log("User not found");
}

Pattern Matching

The match() method provides elegant pattern matching:

const message = user.match({
  onSome: (value) => `User: ${value.name}`,
  onNone: () => "User not found",
});

console.log(message);

Working with Maybe

Default Values

Provide a fallback value when there's none:

const config = getConfig();

const timeout = config.match({
  onSome: (cfg) => cfg.timeout,
  onNone: () => 5000, // default timeout
});

Transforming Values

const name = getUser()
  .match({
    onSome: (user) => user.name,
    onNone: () => "Guest",
  })
  .toUpperCase();

Chaining Operations

const email = findUser(123)
  .match({
    onSome: (user) => user.email,
    onNone: () => Maybe.none(),
  })
  .match({
    onSome: (email) => email,
    onNone: () => "No email available",
  });

Type Safety

Maybe provides excellent type safety with TypeScript:

function getFirstItem<T>(items: T[]): Maybe<T> {
  if (items.length === 0) {
    return Maybe.none();
  }
  return Maybe.some(items[0]);
}

const result = getFirstItem([1, 2, 3]);

if (result.isSome()) {
  // TypeScript knows:
  // - result.value exists and is of type number
  console.log(result.value + 10);
}

Handling Falsy Values

Maybe correctly handles falsy values that are actually present:

// These are all valid Some values
Maybe.some(0);        // number zero
Maybe.some("");       // empty string
Maybe.some(false);    // boolean false

// Only Maybe.none() represents no value

This avoids common bugs where falsy values are confused with missing values.

Best Practices

1. Always Handle Both Cases

Use match() to ensure both cases are handled:

// ✅ Good - explicit handling
maybeValue.match({
  onSome: (value) => console.log(value),
  onNone: () => console.log("No value"),
});

// ⚠️ Be careful - you might forget to handle None
if (maybeValue.isSome()) {
  console.log(maybeValue.value);
}

2. Return None for Missing Values

When a value might not exist, return Maybe:

// ❌ Bad - returns null
function findUser(id: number): User | null {
  // ...
}

// ✅ Good - returns Maybe
function findUser(id: number): Maybe<User> {
  // ...
}

3. Don't Use Maybe for Nullable Values

If you need to distinguish between "no value" and "null value", use a different approach:

// ❌ Confusing - is Maybe.none() different from Maybe.some(null)?
type MaybeNull<T> = Maybe<T | null>;

// ✅ Better - use Result or a custom type
type Nullable<T> = T | null;

4. Use Descriptive Match Handlers

Make it clear what happens in each case:

// ✅ Good - clear intent
const result = findUser(id).match({
  onSome: (user) => `Welcome ${user.name}!`,
  onNone: () => "User not found. Please register.",
});

// ❌ Less clear
const result = findUser(id).match({
  onSome: (u) => u.name,
  onNone: () => "error",
});

Common Patterns

Safe Array Access

function first<T>(array: T[]): Maybe<T> {
  return array.length > 0 ? Maybe.some(array[0]) : Maybe.none();
}

function last<T>(array: T[]): Maybe<T> {
  return array.length > 0 ? Maybe.some(array[array.length - 1]) : Maybe.none();
}

Safe Property Access

function getEmail(user: Maybe<User>): Maybe<string> {
  return user.match({
    onSome: (u) => u.email ? Maybe.some(u.email) : Maybe.none(),
    onNone: () => Maybe.none(),
  });
}

Configuration with Defaults

interface Config {
  timeout?: number;
  retries?: number;
}

function getTimeout(config: Maybe<Config>): number {
  return config.match({
    onSome: (cfg) => cfg.timeout ?? 5000,
    onNone: () => 5000,
  });
}

See Also

  • Result - For operations that can fail with errors
  • Try - For wrapping code that might throw exceptions
  • Outcome - Advanced result system with causes and exceptions

On this page