Skip to Content
Course content

177: Type Aliases vs Interfaces Revisited

Click on the "Edit" button in the top corner of the screen to edit your slide content.

If you've been hanging around TypeScript forums or reading a few blog posts, you've probably encountered the common advice that "Interfaces and Type Aliases are functionally identical for 95% of use cases." I'll be honest: for a beginner, that's a helpful simplification. But as you move into senior-level architecture, treating them as interchangeable is a mistake that will eventually bite you.

The "They're Basically Identical" Myth

The misconception is that the only difference between type and interface is the syntax. People assume that if they can describe an object shape with either, it doesn't matter which one they pick. Let me show you why that's wrong by looking at how TypeScript handles "open" vs "closed" definitions.

// Using a Type Alias
type User = {
  name: string;
  email: string;
};

// This will trigger a TS error: "Duplicate identifier 'User'."
type User = {
  phoneNumber: string;
};

With a type alias, once it's defined, it's locked. You can't just "add" more properties to it later in the same namespace. Now, look at what happens when we use an interface.

// Using an Interface
interface User {
  name: string;
  email: string;
}

// This is perfectly legal! This is called "Declaration Merging."
interface User {
  phoneNumber: string;
}

const me: User = {
  name: "Alex",
  email: "alex@dev.com",
  phoneNumber: "555-0123"
};

Why Declaration Merging Changes the Game

You might be thinking, "Why on earth would I want my types to be accidentally overwritten?" In your internal application code, you probably don't. But if you're writing a library or a shared SDK, declaration merging is a superpower. It allows the consumer of your library to extend your types without having to wrap everything in a new interface.

I've spent hours debugging weird bugs in third-party plugins where the plugin author used type for everything, making it impossible for me to add a custom property to the configuration object without a massive refactor. If you're building a public-facing API, use interfaces for your main object shapes. If you're building a private component and want to ensure the shape remains exactly as defined, a type alias is your best friend.

When Type Aliases Actually Outshine Interfaces

While interfaces win on extensibility, they are fundamentally limited to describing object shapes. Type aliases are far more flexible because they can represent anything. You cannot use an interface to define a union type, which is one of the most powerful features of TypeScript.

// This is impossible with an interface
type Status = 'pending' | 'approved' | 'rejected';

type Response = {
  data: string;
} | {
  error: string;
  code: number;
};

If you tried to force the Response example into an interface, you'd end up with a mess of optional properties (data? and error?), which destroys your type safety. With a union type alias, TypeScript forces you to check which "branch" of the union you're dealing with before you access the properties. I almost always use type for state management and API responses for exactly this reason.




📋 Practical Task

Refactoring the Payment Gateway Type System

You are working on a payment processing module. Currently, the code uses interfaces for everything, which is causing two problems: you can't represent the "Payment Method" as a choice between options, and the PaymentConfig is too rigid for users of your module to extend.

Your Goal: Refactor the following code to use the correct tool for the job.

  • Change PaymentMethod to a type alias that is a union of 'credit_card' | 'paypal' | 'crypto'.
  • Ensure PaymentConfig remains an interface so that other developers can use declaration merging to add their own custom metadata to the config.
  • Create a type alias called PaymentResult that is a union of a Success object (with a transactionId) or a Failure object (with an errorCode).
// REFACTOR THIS CODE
interface PaymentMethod {
  // Currently just a string, make this a union type alias!
  value: string; 
}

interface PaymentConfig {
  apiKey: string;
  timeout: number;
}

// Create a PaymentResult type alias here as a union of Success or Failure

function processPayment(method: PaymentMethod, config: PaymentConfig): any {
  // Implementation logic...
  return { transactionId: "TXN_123" };
}
Rating
0 0

There are no comments for now.

to be the first to leave a comment.