Back to Blog
·4 min read

Why I build with TypeScript interfaces instead of types for public APIs

Interfaces provide better extensibility and clearer intent than type aliases when designing public APIs and component props.

AI Dev
typescript
api-design
interfaces
types

Why I build with TypeScript interfaces instead of types for public APIs

I used to reach for type aliases everywhere in TypeScript. They felt more functional, more modern -- especially coming from languages where types were just aliases. Then I started building libraries and components that other developers would extend, and I learned the hard way that interfaces are not just syntactic sugar. They're fundamentally different tools that solve different problems.

The moment this clicked was when a teammate tried to extend one of my component prop types to add custom data attributes. I'd defined the props with a type alias, and TypeScript threw cryptic errors when they tried to use intersection types to extend it. We spent an hour debugging what should have been a five-minute change, all because I'd chosen type instead of interface.

Types vs Interfaces: More Than Syntax

Here's what I used to write for component props:

type ButtonProps = {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick: (event: MouseEvent) => void;
};

And here's what I write now:

interface ButtonProps {
  variant: 'primary' | 'secondary' | 'danger';
  size: 'small' | 'medium' | 'large';
  disabled?: boolean;
  onClick: (event: MouseEvent) => void;
}

The syntax difference is minimal, but the behavior is completely different. With interfaces, other developers can easily extend my component:

// This works beautifully
interface CustomButtonProps extends ButtonProps {
  'data-testid'?: string;
  icon?: ReactNode;
}
 
// This also works
interface ButtonProps {
  customAttribute?: string; // Declaration merging!
}

Try that with a type alias and you'll get into intersection type hell:

type CustomButtonProps = ButtonProps & {
  'data-testid'?: string;
  icon?: ReactNode;
};
// Works, but gets ugly fast with complex types

When Interfaces Actually Matter

API boundaries are where interfaces shine. Any props interface, configuration object, or data contract that you expect others to extend should be an interface. The declaration merging feature alone makes this worthwhile -- it means library consumers can augment your types without forking your code.

I learned this building an internal component library. Developers needed to add custom props to form components, extend theme configurations, and augment API response types. With interfaces, they could do this cleanly:

// In the library
interface FormFieldProps {
  name: string;
  label: string;
  required?: boolean;
}
 
// In consumer code
declare module '@company/ui' {
  interface FormFieldProps {
    'data-analytics'?: string;
    helpText?: string;
  }
}

That's declaration merging in action -- impossible with type aliases.

Complex object structures also benefit from interfaces. When you're modeling domain objects with multiple properties and potential inheritance relationships, interfaces make the intent clearer:

interface User {
  id: string;
  email: string;
  createdAt: Date;
}
 
interface AdminUser extends User {
  permissions: Permission[];
  lastLoginAt?: Date;
}

When I Still Use Types

I have not abandoned type aliases entirely. They're perfect for computed types and unions:

type Status = 'loading' | 'success' | 'error';
type ApiResponse<T> = { data: T } | { error: string };
type EventHandler<T> = (event: T) => void;

Utility types are another clear win for type aliases:

type Partial<T> = {
  [P in keyof T]?: T[P];
};
 
type UserUpdate = Partial<Pick<User, 'email' | 'name'>>;

These are functional transformations -- they take types as input and produce new types as output. Interfaces can not do this.

The Performance Myth

One argument I hear for preferring types is performance. "Interfaces create overhead" -- this is wrong. TypeScript compiles both interfaces and type aliases away completely. There's zero runtime difference. The TypeScript compiler might work slightly harder with complex interface hierarchies, but we're talking microseconds during development builds.

The real performance impact comes from poor type design -- deeply nested generics, excessive conditional types, or circular references. Whether you use interface or type does not matter for build performance.

My Current Rules

After building dozens of libraries and components, here's my approach:

  • Interfaces for object shapes that might be extended or implemented
  • Interfaces for component props and configuration objects
  • Interfaces for data models and API contracts
  • Types for unions, computations, and utility transformations
  • Types for function signatures and complex type operations

The key insight is thinking about extensibility from day one. If there's even a chance someone might want to extend your type definition, use an interface. You can not predict how your code will be used six months from now, but you can make it easy to extend.

Most of my TypeScript debugging time used to come from fighting the type system when trying to extend or compose types. Choosing interfaces for public APIs eliminated most of these issues. The code is not just more extensible -- it's more maintainable too.