Differences Between Type and Interface in TypeScript

During a recent TypeScript workshop, a developer asked a question I hear all the time: “Should I use type or interface here?”

If you read the docs or AI-generated summaries, you’ll often see “they are mostly the same.” That’s true for simple cases, but it’s not the full story in real-world projects.

In this guide, you’ll learn the practical differences between type and interface in TypeScript, where they behave differently (like declaration merging), and simple rules of thumb you can apply in your own codebase.

We’ll use realistic examples (such as User, Address, and ApiResponse<T>) and cover how teams typically choose between types and interfaces in larger projects.

Quick overview of type vs interface

At a high level:

  • Both type and interface can describe the shape of objects.
  • Interfaces support declaration merging and the extends keyword.
  • Type aliases support unions, intersections, and advanced combinations.
  • In many simple cases, they are interchangeable, but their differences matter as your codebase grows.

A good mental model is:

  • Use interface for object shapes you want to model and share.
  • Use type for unions, intersections, and utility “building block” types.

Extendability: extends, intersections, and merging

Extendability is one of the most important differences to understand.

Interface extension with extends

Interfaces can extend other interfaces using the extends keyword, and they can be reopened across files in some scenarios (via declaration merging, covered later).

interface Person {
firstName: string;
lastName: string;
}

interface Employee extends Person {
employeeId: number;
}

const employee: Employee = {
firstName: "Alice",
lastName: "Johnson",
employeeId: 1234,
};

I executed the above example code and added the screenshot below.

Type vs Interface TypeScript

Here, Employee builds on Person in an object-oriented way.

Type “extension” using intersections

Types cannot be reopened, but you can build on them compositionally using intersections.

interface Person {
firstName: string;
lastName: string;
}

interface Employee extends Person {
employeeId: number;
}

const employee: Employee = {
firstName: "Alice",
lastName: "Johnson",
employeeId: 1234,
};
console.log(employee);

This achieves a similar result but via the type system’s intersection operator (&) instead of extends.

Key takeaway

  • Interfaces support extends and can also participate in declaration merging.
  • Types cannot be reopened, but you can create new types from existing ones using intersections.

Think of interfaces as “named object models” and type aliases as “flexible type expressions.”

Declaration merging (with real-world example)

Declaration merging is something only interfaces can do. It allows multiple declarations of the same interface name to be combined.

Simple example

interface Car {
make: string;
model: string;
}

interface Car {
year: number;
}

const car: Car = {
make: "Tesla",
model: "Model S",
year: 2022,
};

console.log(car);

I executed the above example code and added the screenshot below.

TypeScript Type vs Interface

Here, both Car declarations are merged into a single interface with makemodel, and year.

If you try the same pattern with type, TypeScript will throw an error:

type Car = {
make: string;
};

// Error: Duplicate identifier 'Car'
type Car = {
year: number;
};

Real-world library augmentation example

Declaration merging becomes very powerful when you want to extend a third-party type, such as adding a custom property to an Express Request object.

declare module "express-serve-static-core" {
interface Request {
userId?: string;
}
}

Because Request is an interface, you can augment it in your own codebase. This is a strong reason to favor interfaces for public APIs and shared domain models.

When to use an interface

Interfaces are usually the best choice when you are modeling the shape of objects, especially if those objects will be shared or extended.

You should consider using an interface when:

  • You are defining the shape of domain entities (like UserOrderProduct).
  • You want other modules, packages, or teams to extend the type later.
  • You are working with class-based code or using implements.
  • You expect declaration merging or library augmentation.

Example:

interface Address {
street: string;
city: string;
state: string;
zipCode: string;
}

interface User {
id: number;
name: string;
email: string;
address: Address;
}

const user: User = {
id: 1,
name: "John Doe",
email: "john@example.com",
address: {
street: "123 Main St",
city: "Springfield",
state: "IL",
zipCode: "62704",
},
};

Here, User and Address are natural uses for interfaces because they represent stable object shapes in your domain.

When to use type

Type aliases shine when you need flexibility beyond plain objects.

You should consider using a type alias when:

  • You are defining union types:type Status = “idle” | “loading” | “success” | “error”;
  • You are creating intersections or composing multiple types:type Id = string | number; type WithTimestamps = { createdAt: Date; updatedAt: Date; }; type Entity<TId = Id> = { id: TId; } & WithTimestamps;
  • You are working with tuples, function types, or more advanced type constructs:type Point = [number, number]; type Callback = (value: string) => void;
  • You are defining reusable utility types or wrappers:type ApiResponse<T> = { data: T; error?: string; status: number; };

In short: type aliases are great “building blocks” for complex type structures.

Real-world examples: domain models and API types

Let’s combine interfaces and types in a realistic scenario: modeling a user and an API response.

Domain models with interfaces

interface Address {
street: string;
city: string;
state: string;
zipCode: string;
}

interface User {
id: number;
name: string;
email: string;
address: Address;
}

This is a natural fit for interfaces, since User and Address are core domain objects that might be extended or used in multiple places.

API response wrapper with type

type ApiResponse<T> = {
data: T;
error?: string;
status: number;
};

const userResponse: ApiResponse<User> = {
data: {
id: 1,
name: "John Doe",
email: "john@example.com",
address: {
street: "123 Main St",
city: "Springfield",
state: "IL",
zipCode: "62704",
},
},
status: 200,
};

Here:

  • User is an interface because it models your domain entity.
  • ApiResponse<T> is a type alias because it is a generic helper that wraps any type.

Combine interfaces and types

You can also mix both approaches:

interface Person {
firstName: string;
lastName: string;
age: number;
address: Address;
}

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

type Manager = Employee & {
subordinates: Employee[];
};

const manager: Manager = {
firstName: "Alice",
lastName: "Johnson",
age: 40,
address: {
street: "789 Oak St",
city: "Naperville",
state: "IL",
zipCode: "60540",
},
employeeId: 1,
department: "Engineering",
subordinates: [
{
firstName: "Bob",
lastName: "Brown",
age: 35,
address: {
street: "101 Pine St",
city: "Evanston",
state: "IL",
zipCode: "60201",
},
employeeId: 2,
department: "Engineering",
},
],
};

I executed the above example code and added the screenshot below.

Type and Interface in TypeScript

This example shows how interfaces (PersonEmployee) define base shapes, while a type alias (Manager) creates a richer type via intersection.

Best practices and common mistakes

Over time, teams usually settle on a small set of conventions. Here are practical best practices to follow.

Best practices

  • Use interfaces for public or shared object models:
    • Domain entities (UserOrderProduct).
    • DTOs and API contracts.
    • Types that library consumers might extend.
  • Use type aliases for:
    • Unions (“pending” | “approved” | “rejected”).
    • Intersections and composed types.
    • Utility types, mapped types, and function signatures.
  • Keep it consistent within a project:
    • Agree on conventions in your team’s docs or contribution guidelines.
    • Prefer predictability over micro-optimizing every type.

Common mistakes

  • Treating type and interface as completely identical:
    • They behave differently with declaration merging and in some edge cases.
  • Trying to merge type aliases:
    • Re-declaring the same type name will cause an error; if you find yourself needing this, you likely want an interface instead.
  • Overcomplicating types:
    • Sometimes a simple interface is easier to read and maintain than a deeply nested type alias.

Summary tables

Here is the summary table.

Feature comparison

FeatureInterfaceType
ExtendabilityYes, can extend other interfacesYes, via intersections, but cannot be reopened or merged
Declaration mergingYes, multiple declarations are mergedNo, duplicate declarations cause an error
Primary usageDefining object shapes and public modelsUnions, intersections, utility types, and non-object types
Example use caseUserAddressOrder modelsStatus unions, ApiResponse<T>, function types
CombinationCan extend interfaces and reference type aliasesCan intersect with interfaces or other types
ScenarioRecommendedWhy
Public domain models / DTOsInterfaceEasier to extend and familiar for object modeling
React props or component inputsInterfaceCommon pattern, clear object shapes
Union of string literalsTypeType aliases excel at unions
Utility / helper wrapper typesTypeOften involve generics, unions, intersections
Augmenting third-party declarationsInterfaceRelies on declaration merging

How do I explain this in live workshops

When teaching TypeScript to teams, I start with a simple question: “Who will consume this type?”

If the answer is “other teams, other modules, or external packages,” we default to interface because it is easier to extend and supports declaration merging.

For internal plumbing, helpers, and unions, we freely use type aliases, especially where unions and intersections make the intent clearer.

This mental model helps developers stop overthinking and pick one quickly.

Quick quiz: test your understanding

Try answering these without looking back, then check yourself at the end.

  1. You need a Status that can be “success” | “error” | “loading”. Do you use type or interface?
  2. You are defining a shared UserProfile that other teams will extend in their modules. Type or interface?
  3. You want a generic Paginated<T> wrapper with properties items, total, and page. Type or interface?

Suggested answers:

  1. type (it’s a union of string literals, not an object shape).
  2. interface (a shared object model that others may extend).
  3. Usually type, especially if you plan to compose it with other types, though the interface can also work here.

Conclusion

In practice, the real question isn’t simply “type or interface?” but “how will this type evolve, and who will depend on it?”

Use interfaces for stable, extendable object models and public contracts, and use type aliases for unions, intersections, and utility helpers.

If you follow that rule of thumb and understand declaration merging and extendability, you’ll be able to choose confidently instead of guessing every time you define a new type.

You may also read:

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.