Skip to content

Web Development||10 min read

TypeScript Type vs Interface: When to Use Each (2025)

The definitive guide to choosing between TypeScript's type and interface. Learn when to use each with real-world examples, common patterns, and practical decision guidelines.

By Sameer Khan

If you've written TypeScript for more than a day, you've asked this question: Should I use type or interface?

Most tutorials give you the academic answer ("interfaces are extendable, types are more flexible") but don't tell you when to actually use each in real projects.

After years of writing TypeScript in production, here's my practical guide.

Quick Answer

Use interface for:

  • Object shapes that might be extended or merged
  • Public APIs and library definitions
  • React component props
  • When you want declaration merging

Use type for:

  • Unions, intersections, and computed types
  • Primitives, tuples, and mapped types
  • When you need type aliases
  • Complex type transformations

Rule of thumb: Start with interface for objects, use type for everything else.


The Core Difference

Interfaces: Extendable Contracts

Interfaces define contracts that objects must follow. They're designed to be extended and merged.

interface User {
  name: string;
  email: string;
}

// Can extend
interface Admin extends User {
  role: 'admin';
  permissions: string[];
}

// Declaration merging works
interface User {
  age?: number; // Merged with previous User interface
}

Types: Flexible Type Aliases

Types create aliases for any TypeScript type. They're more flexible but can't be merged.

type User = {
  name: string;
  email: string;
}

// Can create unions
type Status = 'pending' | 'approved' | 'rejected';

// Can create intersections
type Admin = User & {
  role: 'admin';
  permissions: string[];
}

When to Use Interface

1. Object Shapes (Especially Extendable Ones)

Use interface when defining object structures that might be extended:

// Good: Interface for extendable object
interface ApiResponse {
  status: number;
  data: unknown;
}

interface SuccessResponse extends ApiResponse {
  status: 200;
  data: { id: string; name: string };
}

interface ErrorResponse extends ApiResponse {
  status: 400 | 500;
  data: { error: string };
}

Why interface here? You might extend ApiResponse for different response types. Interfaces make this cleaner.

2. React Component Props

Use interface for React component props (community convention):

// Good: Interface for React props
interface ButtonProps {
  label: string;
  onClick: () => void;
  variant?: 'primary' | 'secondary';
  disabled?: boolean;
}

function Button({ label, onClick, variant, disabled }: ButtonProps) {
  return (
    <button onClick={onClick} disabled={disabled} className={variant}>
      {label}
    </button>
  );
}

Why interface here? React community standard. Also, if you're building a component library, interfaces allow declaration merging for theme extensions.

3. Public APIs and Library Definitions

Use interface when defining public APIs that others might extend:

// Good: Interface for library API
interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
}

// Users of your library can extend it
interface MyDatabaseConfig extends DatabaseConfig {
  ssl: boolean;
  connectionPool?: number;
}

Why interface here? Allows library users to extend your types without modifying your code.

4. Declaration Merging Needed

Use interface when you need TypeScript's declaration merging:

// Good: Interface allows merging
interface Window {
  myCustomProperty: string;
}

// Later, elsewhere in your codebase
interface Window {
  anotherProperty: number;
}

// Now window has both properties
window.myCustomProperty = 'hello';
window.anotherProperty = 42;

Why interface here? Types don't support declaration merging. This is useful for augmenting third-party types.


When to Use Type

1. Unions and Intersections

Use type for unions, intersections, and computed types:

// Good: Type for unions
type Status = 'pending' | 'approved' | 'rejected';

type UserRole = 'admin' | 'user' | 'guest';

// Good: Type for intersections
type AdminUser = User & {
  role: 'admin';
  permissions: string[];
}

// Good: Type for complex unions
type ApiResponse = 
  | { status: 200; data: User }
  | { status: 400; error: string }
  | { status: 500; error: string };

Why type here? Interfaces can't represent unions directly. Types are the right tool for this.

2. Primitives, Tuples, and Mapped Types

Use type for non-object types:

// Good: Type for primitives
type ID = string | number;

// Good: Type for tuples
type Coordinate = [number, number];

type Point3D = [number, number, number];

// Good: Type for mapped types
type Optional<T> = {
  [K in keyof T]?: T[K];
}

type ReadonlyUser = Readonly<User>;

Why type here? Interfaces only work for object shapes. For primitives, tuples, and mapped types, you need type.

3. Type Aliases and Computed Types

Use type when creating aliases or computed types:

// Good: Type alias for clarity
type UserID = string;
type Email = string;

function getUser(id: UserID): User {
  // ...
}

// Good: Computed types
type Keys = keyof User; // 'name' | 'email' | 'age'

type UserValues = User[keyof User]; // string | number | undefined

// Good: Conditional types
type NonNullable<T> = T extends null | undefined ? never : T;

Why type here? These are type-level operations that interfaces can't handle.

4. Complex Type Transformations

Use type for complex type manipulations:

// Good: Complex type transformations
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
}

type RequiredFields<T, K extends keyof T> = T & {
  [P in K]-?: T[P];
}

// Usage
type UserWithRequiredEmail = RequiredFields<User, 'email'>;

Why type here? Interfaces can't do these advanced type manipulations.


Common Patterns and Examples

Pattern 1: Extending vs Intersecting

Interface approach (extending):

interface BaseUser {
  id: string;
  name: string;
}

interface AdminUser extends BaseUser {
  role: 'admin';
  permissions: string[];
}

Type approach (intersecting):

type BaseUser = {
  id: string;
  name: string;
}

type AdminUser = BaseUser & {
  role: 'admin';
  permissions: string[];
}

When to use which?

  • Use interface if you might extend BaseUser in multiple ways
  • Use type if you're doing one-off intersections or combining multiple types

Pattern 2: React Component Props

Standard approach (interface):

interface CardProps {
  title: string;
  children: React.ReactNode;
  onClick?: () => void;
}

function Card({ title, children, onClick }: CardProps) {
  return (
    <div onClick={onClick}>
      <h2>{title}</h2>
      {children}
    </div>
  );
}

Why interface? React community convention, and allows for prop extensions if needed.

Pattern 3: API Response Types

Using type for discriminated unions:

type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

// Usage with type narrowing
function handleResponse<T>(response: ApiResponse<T>) {
  if (response.success) {
    // TypeScript knows response.data exists here
    console.log(response.data);
  } else {
    // TypeScript knows response.error exists here
    console.error(response.error);
  }
}

Why type? Discriminated unions work better with type. This pattern gives you excellent type safety.

Pattern 4: Configuration Objects

Using interface for extendable config:

interface DatabaseConfig {
  host: string;
  port: number;
  database: string;
}

interface ProductionConfig extends DatabaseConfig {
  ssl: true;
  connectionPool: 10;
}

interface DevelopmentConfig extends DatabaseConfig {
  ssl: false;
  connectionPool: 1;
}

Why interface? Multiple config types extending a base makes sense with interfaces.


Performance Considerations

Myth: "Interfaces are faster than types"

Reality: There's no runtime performance difference. Both are compile-time only. TypeScript erases all type information during compilation.

The only difference is:

  • Interfaces are checked earlier in the compilation process (can be slightly faster to compile)
  • Types are more flexible but might take slightly longer to check complex transformations

Verdict: Performance shouldn't influence your choice. Use the right tool for the job.


Decision Flowchart

Is it an object shape?
├─ Yes → Will it be extended or merged?
│   ├─ Yes → Use interface
│   └─ No → Either works, prefer interface for consistency
│
└─ No → Is it a union, intersection, or computed type?
    ├─ Yes → Use type
    └─ No → Is it a primitive, tuple, or mapped type?
        ├─ Yes → Use type
        └─ No → Use type (default for non-objects)

Common Mistakes

Mistake 1: Using Interface for Unions

// Bad: Interface can't represent unions
interface Status = 'pending' | 'approved'; // Error!

// Good: Use type for unions
type Status = 'pending' | 'approved';

Mistake 2: Using Type When Declaration Merging is Needed

// Bad: Type doesn't support declaration merging
type Window = {
  myProperty: string;
}

type Window = { // Error: Duplicate identifier
  anotherProperty: number;
}

// Good: Use interface for merging
interface Window {
  myProperty: string;
}

interface Window {
  anotherProperty: number; // Merged!
}

Mistake 3: Inconsistent Usage in Same Codebase

// Bad: Mixing interface and type for similar things
interface User {
  name: string;
}

type Product = {
  name: string;
}

// Good: Be consistent
interface User {
  name: string;
}

interface Product {
  name: string;
}

Best Practices Summary

  1. Start with interface for object shapes - It's more extendable and follows common conventions
  2. Use type for unions, intersections, and computed types - Interfaces can't handle these
  3. Be consistent - Pick one approach for similar structures in your codebase
  4. Use interface for React props - Community standard
  5. Use type for type aliases - That's what they're designed for
  6. Don't overthink it - Both work for simple object shapes; consistency matters more

Real-World Example: Building a Type System

Here's how I'd structure types for a real application:

// Base types (interfaces for extendability)
interface BaseEntity {
  id: string;
  createdAt: Date;
  updatedAt: Date;
}

interface User extends BaseEntity {
  name: string;
  email: string;
  role: UserRole;
}

// Unions and computed types (types)
type UserRole = 'admin' | 'user' | 'guest';

type ApiResponse<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

type UserKeys = keyof User; // 'id' | 'name' | 'email' | ...

// React props (interfaces)
interface UserCardProps {
  user: User;
  onEdit?: (user: User) => void;
  showActions?: boolean;
}

// Complex transformations (types)
type PartialUser = Partial<User>;
type UserWithoutTimestamps = Omit<User, 'createdAt' | 'updatedAt'>;

Key Takeaways

  1. Interfaces are for object shapes that might be extended or merged
  2. Types are for unions, intersections, primitives, and computed types
  3. React props → use interface (convention)
  4. Unions/intersections → use type (required)
  5. Consistency matters more than the specific choice for simple objects
  6. No performance difference - both are compile-time only

The "type vs interface" debate often overcomplicates things. In practice:

  • Use interface for objects (especially extendable ones)
  • Use type for everything else
  • Be consistent in your codebase

That's it. No need to overthink it.


FAQ

Q: Can I mix interface and type in the same codebase? A: Yes, but be consistent. Use interface for similar object structures and type for similar non-object types.

Q: Which is better for performance? A: Neither. Both are compile-time only and have no runtime impact.

Q: Should I convert all my types to interfaces? A: No. Use the right tool for each situation. Types are better for unions and computed types.

Q: Can interfaces extend types? A: Yes, but types can't extend interfaces. You can use intersection types instead.

Q: When should I use declaration merging? A: When augmenting third-party types (like Window or library types) or building extensible APIs.