ShaharAmir
← Back to Blog
TypeScript6 min read

Types vs Interfaces in TypeScript — Which One Should You Use?

The real differences between type and interface, when each one wins, and a practical decision guide

S
Shahar Amir

Types vs Interfaces in TypeScript

This is probably the most asked TypeScript question ever. And most answers online are either outdated or oversimplified.

Let's settle it once and for all.

The Basics

Both can describe the shape of an object:

// Using interface
interface User {
  id: number;
  name: string;
  email: string;
}

// Using type
type Product = {
  id: number;
  title: string;
  price: number;
};

// Both work the same way
const user: User = { id: 1, name: "Shahar", email: "shahar@dev.com" };
const product: Product = { id: 1, title: "Keyboard", price: 99 };

export default function App() {
  return (
    <div style={{ fontFamily: "monospace", padding: 16 }}>
      <p>User: {user.name} ({user.email})</p>
      <p>Product: {product.title} — ${product.price}</p>
    </div>
  );
}

So far, identical. Here's where they diverge.

What Only type Can Do

Union Types

typescript
123456
type Status = "idle" | "loading" | "success" | "error";
type StringOrNumber = string | number;
// ❌ Interfaces can't do this
// interface Status = "idle" | "loading" // Syntax error

Mapped Types

typescript
1234567
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};

Tuple Types

typescript
1234
type Coordinates = [number, number];
type APIResponse = [data: User[], error: string | null];
// ❌ Interfaces can't express tuples cleanly

Conditional Types

typescript
1234
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false

Extracting Types from Values

typescript
12345678
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
retries: 3,
} as const;
type Config = typeof config;
// { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000; readonly retries: 3 }

What Only interface Can Do

Declaration Merging

typescript
123456789101112131415
// First declaration
interface Window {
title: string;
}
// Second declaration — merges with the first!
interface Window {
customProperty: boolean;
}
// Window now has both `title` and `customProperty`
const w: Window = {
title: "My App",
customProperty: true,
};

This is actually useful for extending third-party types (like adding properties to Window or Request).

Types can't merge:

typescript
12
type User = { name: string };
type User = { email: string }; // ❌ Error: Duplicate identifier

Extends (Cleaner Inheritance)

typescript
123456789101112131415
interface Animal {
name: string;
age: number;
}
interface Dog extends Animal {
breed: string;
bark(): void;
}
// Types use intersection instead:
type Cat = Animal & {
indoor: boolean;
purr(): void;
};

Both work, but extends gives better error messages when types conflict.

Performance Difference

Here's something most people don't know — interfaces are slightly faster for the TypeScript compiler.

typescript
1234567891011
// ✅ Interface — compiler caches the shape
interface UserProps {
name: string;
avatar: string;
role: "admin" | "user";
}
// 🐢 Type intersection — evaluated every time
type UserProps = BaseProps & {
role: "admin" | "user";
} & AvatarProps;

For small projects you'll never notice. For large codebases with hundreds of types, interface caching can make a difference in IDE responsiveness and compile times.

The Decision Guide

export default function App() {
  const rules = [
    { need: "Object shapes / props", use: "Either ✅", winner: "tie" },
    { need: "Union types", use: "type", winner: "type" },
    { need: "Mapped / conditional types", use: "type", winner: "type" },
    { need: "Tuples", use: "type", winner: "type" },
    { need: "Declaration merging", use: "interface", winner: "interface" },
    { need: "extends (inheritance)", use: "interface", winner: "interface" },
    { need: "Compiler performance", use: "interface", winner: "interface" },
    { need: "React component props", use: "Either (prefer interface)", winner: "interface" },
  ];

  return (
    <div style={{ fontFamily: "monospace", fontSize: 13, padding: 16 }}>
      <h3 style={{ margin: "0 0 12px" }}>🧭 Decision Guide</h3>
      {rules.map((r, i) => (
        <div key={i} style={{
          display: "flex", justifyContent: "space-between",
          padding: "6px 8px", background: i % 2 === 0 ? "#1e1e2e" : "transparent",
          borderRadius: 4, color: "#e0e0e0"
        }}>
          <span>{r.need}</span>
          <span style={{
            color: r.winner === "type" ? "#7dcfff" : r.winner === "interface" ? "#9ece6a" : "#bb9af7",
            fontWeight: "bold"
          }}>{r.use}</span>
        </div>
      ))}
    </div>
  );
}

My Recommendation

Here's what I actually do in production:

typescript
12345678910111213141516
// ✅ Use interface for object shapes and React props
interface ButtonProps {
label: string;
variant: "primary" | "secondary";
onClick: () => void;
}
// ✅ Use type for everything else
type Status = "idle" | "loading" | "success" | "error";
type Handler = (event: MouseEvent) => void;
type Nullable<T> = T | null;
type APIResponse<T> = {
data: T;
error: string | null;
status: number;
};

The simple rule: Use interface for objects and props, use type for unions, utilities, and anything complex.

Don't overthink it. Both are fine. Pick one convention for objects and stick with it across your codebase. Consistency beats the marginal difference.

Common Gotchas

Gotcha 1: extends vs & Error Messages

typescript
12345678910111213
interface A {
x: number;
}
// Interface extends — clear error
interface B extends A {
x: string; // ❌ Error: Type 'string' is not assignable to type 'number'
}
// Type intersection — silent problem
type C = A & {
x: string; // No error... but x is now `never` (number & string = never)
};

Interfaces catch conflicts. Intersections hide them.

Gotcha 2: Interfaces Are Open

typescript
123456789
// Someone else's code or a library:
interface Config {
debug: boolean;
}
// Your code accidentally merges into it:
interface Config {
debug: string; // 💥 Now debug is boolean AND string
}

This is why some teams prefer type for internal code — it's sealed by default.

TL;DR

  • interface = objects, props, class shapes, public APIs
  • type = unions, tuples, mapped types, utilities, complex types
  • Performance: interfaces are slightly faster in the compiler
  • Safety: types can't be accidentally merged
  • Just pick one for objects and be consistent

Both are great. The worst choice is switching randomly between them with no convention.

#types#interfaces#fundamentals

Stay Updated 📬

Get the latest tips and tutorials delivered to your inbox. No spam, unsubscribe anytime.