# TypeScript Types vs Interfaces

TypeScript offers two primary ways to define object shapes: types and interfaces. While they often appear interchangeable, understanding their differences helps you write more maintainable TypeScript code and choose the right tool for each situation.

This guide explores the practical differences between types and interfaces, their unique capabilities, and when to use each in your TypeScript projects.

<iframe width="100%" height="315" src="https://www.youtube.com/embed/vcVoyLQMCxU" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>

## What are TypeScript interfaces?

Interfaces in TypeScript define the structure of objects. They describe what properties an object should have and what types those properties should be:

```typescript
[label user.ts]
interface User {
  id: number;
  name: string;
  email: string;
  isActive: boolean;
}

const user: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
  isActive: true
};
```

Interfaces are particularly useful when you need to describe the shape of objects throughout your application. They provide clear contracts that your code must follow, and TypeScript will enforce these contracts at compile time. When you try to assign a value that doesn't match the interface structure, TypeScript immediately catches the error before your code runs.

The name "interface" comes from object-oriented programming, where interfaces define contracts that classes must implement. In TypeScript, interfaces serve a broader purpose beyond just class contracts. They're commonly used to type plain JavaScript objects, function parameters, and return values.

You can also define optional properties and readonly properties in interfaces:

```typescript
[label optional-readonly.ts]
interface User {
  readonly id: number;
  name: string;
  email: string;
  phone?: string;
  address?: {
    street: string;
    city: string;
    country: string;
  };
}

const user: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com"
};

// user.id = 2; // Error: Cannot assign to 'id'
```

The `readonly` modifier prevents properties from being modified after the object is created. This is useful for immutable data structures or when you want to ensure certain values remain constant. Optional properties, marked with `?`, allow you to define properties that may or may not be present on an object.

Interfaces also support index signatures, which allow you to define objects with dynamic property names:

```typescript
[label index-signatures.ts]
interface StringMap {
  [key: string]: string;
}

const translations: StringMap = {
  hello: "Hola",
  goodbye: "Adiós",
  thanks: "Gracias"
};

interface NumberDictionary {
  [index: string]: number;
  length: number;
  name: string; // Error: must be number
}
```

## What are TypeScript types?

Type aliases create names for any type, not just object shapes. They're more flexible than interfaces and can represent primitives, unions, intersections, and complex type compositions:

```typescript
[label types.ts]
type UserID = number;
type Status = "active" | "inactive" | "pending";

type User = {
  id: UserID;
  name: string;
  status: Status;
};
```

The key word here is "alias"—types create alternative names for existing types. This might seem simple, but it becomes powerful when you need to create complex type definitions or reuse type patterns across your application.

Types shine when you need to create union types, intersection types, or alias primitive types for better code readability. They can represent any valid TypeScript type, making them incredibly versatile:

```typescript
[label type-flexibility.ts]
type ID = string | number;
type Coordinates = [number, number];
type Callback = (data: string) => void;

type ApiResponse<T> = {
  data: T;
  status: number;
  headers: Record<string, string>;
};
```

One of the most powerful aspects of type aliases is their ability to represent things that interfaces simply cannot. While interfaces are restricted to describing object shapes, types can describe any valid TypeScript type, including primitives, unions, tuples, and more complex type expressions.

Types also support all the same object shape features as interfaces, including optional and readonly properties:

```typescript
[label type-objects.ts]
type Product = {
  readonly id: string;
  name: string;
  price: number;
  description?: string;
  tags: string[];
};

const product: Product = {
  id: "prod-123",
  name: "Laptop",
  price: 999,
  tags: ["electronics", "computers"]
};
```

## Types vs interfaces: a quick comparison

Understanding when to use each requires knowing their capabilities and limitations:

| Feature                 | Interface       | Type                 |
| ----------------------- | --------------- | -------------------- |
| Object shape definition | Yes             | Yes                  |
| Extending other types   | Via `extends`   | Via `&` intersection |
| Declaration merging     | Yes             | No                   |
| Union types             | No              | Yes                  |
| Intersection types      | No              | Yes                  |
| Primitive type aliases  | No              | Yes                  |
| Tuple types             | No              | Yes                  |
| Mapped types            | No              | Yes                  |
| Conditional types       | No              | Yes                  |
| Function types          | Yes             | Yes                  |
| Class implementation    | Yes             | Yes                  |
| Performance             | Slightly faster | Slightly slower      |
| Error messages          | Often clearer   | Can be verbose       |

This comparison reveals that types are technically more powerful since they can do everything interfaces can do, plus additional features. However, this doesn't mean you should always use types. Interfaces have their own advantages, particularly around declaration merging, performance, and readability.

## Extending and composing types

Both interfaces and types support extension and composition, but they handle it differently. Understanding these differences helps you choose the right approach for your specific needs.

Interfaces use the `extends` keyword to inherit from other interfaces:

```typescript
[label interfaces.ts]
interface Person {
  name: string;
  age: number;
}

interface Employee extends Person {
  employeeId: number;
  department: string;
}

const employee: Employee = {
  name: "Jane Smith",
  age: 30,
  employeeId: 12345,
  department: "Engineering"
};
```

The `extends` keyword creates a clear inheritance relationship. When you extend an interface, you're creating a new interface that includes all properties from the base interface plus any additional properties you define. This is intuitive for developers coming from object-oriented programming backgrounds.

You can extend multiple interfaces at once, which is useful for composing functionality from different sources:

```typescript
[label multiple-extends.ts]
interface Timestamped {
  createdAt: Date;
  updatedAt: Date;
}

interface Identifiable {
  id: string;
}

interface Deletable {
  deletedAt?: Date;
  isDeleted: boolean;
}

interface User extends Identifiable, Timestamped, Deletable {
  name: string;
  email: string;
}
```

This composition pattern is common in domain modeling where entities share common traits. For example, most database entities need timestamps and unique identifiers. Rather than repeating these properties across multiple interfaces, you can define them once and extend them wherever needed.

Types use intersection types (`&`) to combine multiple types:

```typescript
[label types-composition.ts]
type Person = {
  name: string;
  age: number;
};

type Employee = Person & {
  employeeId: number;
  department: string;
};

const employee: Employee = {
  name: "Jane Smith",
  age: 30,
  employeeId: 12345,
  department: "Engineering"
};
```

Intersection types work by combining all properties from multiple types into a single type. The resulting type must satisfy all constituent types simultaneously. While this achieves similar results to interface extension, the syntax and mental model are slightly different.

Types can also combine with union types for more complex compositions:

```typescript
[label complex-types.ts]
type BasePost = {
  title: string;
  content: string;
  author: string;
};

type DraftPost = BasePost & {
  status: "draft";
  lastEdited: Date;
};

type PublishedPost = BasePost & {
  status: "published";
  publishedAt: Date;
  views: number;
};

type ArchivedPost = BasePost & {
  status: "archived";
  archivedAt: Date;
  reason: string;
};

type Post = DraftPost | PublishedPost | ArchivedPost;
```

This pattern creates what's called a discriminated union. The `status` property acts as a discriminator, allowing TypeScript to narrow the type based on its value. When you check `post.status === "published"`, TypeScript knows the post has `publishedAt` and `views` properties.

The flexibility of combining intersections and unions is one of the main advantages of types over interfaces. You can model complex domain logic directly in the type system, catching errors at compile time that would otherwise only appear at runtime.

## Declaration merging with interfaces

One unique feature of interfaces is declaration merging. When you declare an interface with the same name multiple times, TypeScript automatically merges them into a single interface:

```typescript
[label declaration-merging.ts]
interface User {
  id: number;
  name: string;
}

interface User {
  email: string;
}

interface User {
  createdAt: Date;
}

// TypeScript merges all three declarations
const user: User = {
  id: 1,
  name: "John Doe",
  email: "john@example.com",
  createdAt: new Date()
};
```

At first glance, declaration merging might seem like a bug waiting to happen. Why would you want to declare the same interface multiple times? However, this feature becomes incredibly valuable in specific scenarios, particularly when working with third-party libraries or building extensible systems.

This feature is particularly useful when extending third-party library types or when building plugin systems. For example, extending the Express Request object to add custom properties:

```typescript
[label extend-library.ts]
// Extending Express Request type
declare global {
  namespace Express {
    interface Request {
      user?: {
        id: string;
        email: string;
        roles: string[];
      };
      requestId: string;
      startTime: number;
    }
  }
}

// Now Request has the user, requestId, and startTime properties
app.use((req, res, next) => {
  req.requestId = generateId();
  req.startTime = Date.now();
  next();
});

app.get('/profile', (req, res) => {
  const userId = req.user?.id;
  const duration = Date.now() - req.startTime;
  console.log(`Request ${req.requestId} took ${duration}ms`);
});
```

This pattern is essential when working with Express middleware. Each middleware might add different properties to the request object, and declaration merging allows you to extend the type definition incrementally across different files.

This is also how popular libraries like `@types/node` add type definitions incrementally across multiple files. Each file can declare the same interface, and TypeScript merges them all together. The Node.js types are split across dozens of files, each declaring parts of global interfaces like `NodeJS.ProcessEnv` or `NodeJS.Global`.

Another common use case is plugin systems. If you're building a framework that supports plugins, declaration merging allows plugin authors to extend your core types:

```typescript
[label plugin-system.ts]
// Core framework
interface PluginAPI {
  registerCommand(name: string, handler: Function): void;
}

// Plugin A adds methods
interface PluginAPI {
  logInfo(message: string): void;
  logError(error: Error): void;
}

// Plugin B adds more methods
interface PluginAPI {
  showNotification(text: string): void;
}

// All plugins' methods are now available
const api: PluginAPI = {
  registerCommand: (name, handler) => {},
  logInfo: (msg) => console.log(msg),
  logError: (err) => console.error(err),
  showNotification: (text) => alert(text)
};
```

Types don't support declaration merging. Attempting to declare a type twice results in an error:

```typescript
[label type-error.ts]
type User = {
  id: number;
  name: string;
};

// Error: Duplicate identifier 'User'
type User = {
  email: string;
};
```

This is by design. Types are meant to be explicit and complete in a single declaration. While this limits flexibility in certain scenarios, it also makes code more predictable and easier to understand at a glance.

## Union and intersection types

Types excel at creating unions and intersections, which interfaces can't do directly. These are fundamental building blocks for modeling complex domain logic in TypeScript.

Union types represent values that can be one of several types:

```typescript
[label unions.ts]
type Success = {
  status: "success";
  data: any;
};

type Error = {
  status: "error";
  message: string;
  code: number;
};

type Result = Success | Error;

function handleResult(result: Result) {
  if (result.status === "success") {
    console.log(result.data);
  } else {
    console.log(`Error ${result.code}: ${result.message}`);
  }
}
```

Union types are particularly useful for representing mutually exclusive states. In the example above, a result is either successful or failed—it can't be both simultaneously. TypeScript enforces this constraint at compile time.

This pattern is incredibly useful for discriminated unions, where TypeScript can narrow types based on a common property:

```typescript
[label discriminated-unions.ts]
type Circle = {
  kind: "circle";
  radius: number;
};

type Rectangle = {
  kind: "rectangle";
  width: number;
  height: number;
};

type Triangle = {
  kind: "triangle";
  base: number;
  height: number;
};

type Shape = Circle | Rectangle | Triangle;

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "rectangle":
      return shape.width * shape.height;
    case "triangle":
      return (shape.base * shape.height) / 2;
  }
}
```

The `kind` property serves as a discriminator. Once you check its value, TypeScript knows exactly which type you're working with and gives you access to the appropriate properties. This technique is also called tagged unions or algebraic data types.

Discriminated unions are exhaustive by default. If you add a new shape type but forget to handle it in your `switch` statement, TypeScript will report an error. This makes your code more maintainable and less prone to bugs as your application evolves.

Union types are also useful for representing loading states in applications:

```typescript
[label loading-states.ts]
type LoadingState<T> =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: T }
  | { status: "error"; error: string };

type UserState = LoadingState<User>;

function UserProfile({ state }: { state: UserState }) {
  switch (state.status) {
    case "idle":
      return <div>Click to load user</div>;
    case "loading":
      return <div>Loading...</div>;
    case "success":
      return <div>Welcome, {state.data.name}!</div>;
    case "error":
      return <div>Error: {state.error}</div>;
  }
}
```

This pattern makes impossible states impossible to represent. You can't have `status: "success"` without data, or `status: "error"` without an error message. The type system enforces these constraints automatically.

Intersection types combine multiple types into one, requiring the value to satisfy all types:

```typescript
[label intersections.ts]
type Draggable = {
  drag: () => void;
  dragStart: (x: number, y: number) => void;
  dragEnd: () => void;
};

type Resizable = {
  resize: (width: number, height: number) => void;
  minWidth: number;
  minHeight: number;
};

type Rotatable = {
  rotate: (degrees: number) => void;
  currentRotation: number;
};

type UIElement = Draggable & Resizable & Rotatable & {
  render: () => void;
  id: string;
};
```

Intersection types are useful for composing behavior from multiple sources. In this example, `UIElement` combines dragging, resizing, and rotating capabilities into a single type. Any value of type `UIElement` must implement all these properties and methods.

The difference between unions and intersections can be confusing at first. Unions represent "either/or" relationships (A or B), while intersections represent "both/and" relationships (A and B). Unions typically make types broader (accepting more values), while intersections make types narrower (requiring more properties).

## Primitive type aliases

Types can create aliases for primitive types, making your code more self-documenting and easier to refactor:

```typescript
[label primitives.ts]
type UserID = string;
type Timestamp = number;
type EmailAddress = string;
type PositiveNumber = number;

type User = {
  id: UserID;
  email: EmailAddress;
  createdAt: Timestamp;
  loginCount: PositiveNumber;
};

function getUserById(id: UserID): User | null {
  return null;
}

function sendEmail(
  to: EmailAddress,
  subject: string,
  body: string
): void {
  // Implementation
}
```

These aliases don't provide runtime validation (the types are erased after compilation), but they make your code more readable and self-documenting. When you see `EmailAddress` instead of `string`, you immediately know what kind of string is expected.

Primitive type aliases also make refactoring easier. If you later decide to change `UserID` from `string` to `number`, you only need to update it in one place. All functions that use `UserID` automatically get the new type.

This technique is particularly valuable in large codebases where the same primitive types are used throughout the application. Instead of scattering string and number types everywhere, you create semantic aliases that convey meaning:

```typescript
[label semantic-aliases.ts]
type Milliseconds = number;
type Percentage = number;
type Degrees = number;
type Pixels = number;

function delay(duration: Milliseconds): Promise<void> {
  return new Promise(resolve => setTimeout(resolve, duration));
}

function rotate(element: Element, angle: Degrees): void {
  element.style.transform = `rotate(${angle}deg)`;
}

function setOpacity(element: Element, opacity: Percentage): void {
  element.style.opacity = String(opacity / 100);
}
```

While `Milliseconds`, `Percentage`, and `Degrees` are all just numbers at runtime, the type aliases make the code's intent crystal clear. You're less likely to accidentally pass degrees where milliseconds are expected.

Interfaces can't create aliases for primitives:

```typescript
[label interface-primitives.ts]
// Error: An interface can only extend an object type
interface UserID extends string {}
```

This limitation stems from how interfaces work. They're designed specifically for describing object shapes, not for aliasing primitive types or creating more complex type expressions.

## When to use interfaces vs types

After understanding all the differences, here's practical guidance on when to use each. The choice often comes down to specific requirements and team conventions.

**Use interfaces when:**

You're defining object shapes that might be extended later. Interfaces are perfect for public APIs where consumers might want to extend your types:

```typescript
[label when-interfaces.ts]
// Library code
export interface PluginOptions {
  enabled: boolean;
  config: Record<string, any>;
}

// Consumers can extend via declaration merging
```

Declaration merging makes interfaces ideal for plugin systems, library APIs, and any scenario where third-party code might need to extend your types.

You're working with classes and want to define contracts:

```typescript
[label interface-contracts.ts]
interface Repository<T> {
  find(id: string): Promise<T | null>;
  save(entity: T): Promise<void>;
  delete(id: string): Promise<void>;
  findAll(): Promise<T[]>;
}

class UserRepository implements Repository<User> {
  async find(id: string) { /* ... */ }
  async save(user: User) { /* ... */ }
  async delete(id: string) { /* ... */ }
  async findAll() { /* ... */ }
}
```

Interfaces feel natural for defining class contracts, especially for developers familiar with object-oriented programming.

You want better IDE autocomplete and clearer error messages for object types. Interfaces are optimized for these scenarios and generally provide a better developer experience when working with object shapes.

**Use types when:**

You need unions, intersections, or any advanced type features:

```typescript
[label when-types.ts]
type Result<T> = Success<T> | Failure;
type Middleware = (req: Request, res: Response) => void | Promise<void>;
type DeepPartial<T> = { [K in keyof T]?: DeepPartial<T[K]> };
```

These patterns are impossible with interfaces. Any time you need type-level computation or complex type transformations, types are your only option.

You're creating utility types or type helpers:

```typescript
[label type-helpers.ts]
type Nullable<T> = T | null;
type ValueOf<T> = T[keyof T];
type Mutable<T> = { -readonly [K in keyof T]: T[K] };
type NonOptional<T> = { [K in keyof T]-?: T[K] };
```

Utility types leverage advanced type features that only type aliases support.

You're aliasing primitives or creating type combinations:

```typescript
[label type-aliases.ts]
type ID = string | number;
type Timestamp = number;
type Coordinates = [number, number];
type Status = "active" | "inactive" | "suspended";
```

These simple aliases improve code readability and maintainability.

## Final thoughts

Types and interfaces in TypeScript both define contracts, but they shine in different scenarios. **Interfaces are best for object shapes, class contracts, and public APIs that might be extended.** Types are ideal for unions, intersections, and advanced type utilities.

Modern TypeScript has made type aliases very powerful. **Types can express most patterns that interfaces can, plus more advanced behavior.** This is why many developers lean toward types for flexibility.

Interfaces are still valuable in the right context. **They work especially well in large or extensible codebases where declaration merging and clear error messages help.**

As a rule of thumb, use interfaces for simple object shapes and class implementations, and types for advanced type logic. **Consistency in your codebase matters more than always choosing one over the other.**