DEV Community

Fedar Haponenka
Fedar Haponenka

Posted on

Why You Should Avoid Optional Properties in TypeScript Interfaces

TypeScript's optional properties, marked with a ?, seem like a convenient way to design flexible interfaces. They promise backward compatibility and the ability to create objects incrementally. However, overusing them is an anti-pattern that often leads to fragile, bug-prone code. What appears to be flexibility is frequently a lack of design clarity.

The Illusion of Flexibility

Consider a typical user interface:

interface User {
  id: string;
  name: string;
  email?: string;
  phoneNumber?: string;
  avatarUrl?: string;
}
Enter fullscreen mode Exit fullscreen mode

This seems harmless, but it creates a fundamental problem: we no longer know what a valid User is. Does a valid user require an email? The type system can't tell us. This ambiguity spreads through the entire application.

The Cascading Complexity

Optional properties push the burden of validation from the type system to runtime checks and developer vigilance. Every function that consumes this interface must handle the possibility of missing data.

function sendWelcomeNotification(user: User) {
  // This code is now littered with guards
  if (user.email) {
    sendEmail(user.email, template);
    return;
  }

  if (user.phoneNumber) {
    sendSms(user.phoneNumber, template);
    return;
  } 

  // What now? Log an error? Throw an exception?
  // The type system gave us no guidance.
}
Enter fullscreen mode Exit fullscreen mode

This violates the core purpose of TypeScript: to use types to eliminate entire categories of runtime errors.

The Null Object Pattern and Explicit Design

Often, the need for an optional property reveals an unstated requirement for a default value or a state machine. Instead of a single, partially-defined interface, use separate, fully-defined interfaces that represent different states.

// A user that has completed core registration
interface RegisteredUser {
  id: string;
  name: string;
}

// A user that has a confirmed contact method
interface ContactableUser extends RegisteredUser {
  email: string; // This is now required
}

// A user with a full profile
interface CompleteUser extends ContactableUser {
  phoneNumber: string;
  avatarUrl: string;
}
Enter fullscreen mode Exit fullscreen mode

This approach forces you to answer critical design questions up front. What is the minimum data required for a user to be considered "registered"? When is an email truly mandatory?

For properties that genuinely might not exist, be explicit with union types or use the null object pattern.

// Instead of `avatarUrl?: string;`
interface User {
  id: string;
  name: string;
  avatarUrl: string | null; // Explicitly marked as potentially absent
}
Enter fullscreen mode Exit fullscreen mode

When Optional Properties Are Acceptable

There are legitimate use cases for optional properties, primarily at the boundaries of your system, such as:

  • Configuration Objects: Where many settings have sensible defaults.

  • Data Transfer Objects (DTOs) for PATCH operations: Where you only want to update provided fields.

  • Third-Party API Responses: Where you cannot control the data shape.

Even in these cases, validate and convert these optional inputs into required properties in your core domain models as soon as possible.

Embrace Intentional Design

Optional properties are a code smell that often indicates unresolved business logic. By favoring required properties and explicit state modeling, you create a type system that works for you, not against you. You'll write code that is self-documenting, less prone to runtime errors, and easier to refactor.
Make your interfaces assertions about what data must be present, and let the compiler guarantee it for you. Your future self, and your teammates, will thank you for the clarity.

Top comments (0)