TypeScript Interfaces vs Type Aliases: Key Differences

TypeScript gives you two common ways to describe object shapes and other custom types: interface and type. They overlap in many everyday cases, but they are not identical, and choosing the right one affects readability, extensibility, and how you model your data.

Quick answer: Use interface when you are describing the shape of an object or a class contract, especially if you may extend it later. Use type when you need unions, intersections, tuples, primitives, or more flexible type expressions.

Difficulty: Beginner

You'll understand this better if you know: basic TypeScript syntax, object literals, and how properties and functions are typed.

1. What Are Interfaces vs Type Aliases?

interface and type both let you give a name to a TypeScript type. In practice, they are often used to describe object shapes, but they solve slightly different problems.

For beginners, the simplest way to think about it is this: interface is the more specialized tool for objects, while type is the more general tool.

2. Why This Choice Matters

The difference matters because TypeScript is used to model real JavaScript code, and real code evolves. Some types need to be extended by other parts of an application. Others need to represent unions, derived shapes, or values that are not plain objects.

Choosing well can make your code easier to read, easier to refactor, and less confusing for your team. It can also prevent awkward workarounds later, especially when you need unions, inheritance-like extension, or merging across multiple files.

3. Basic Syntax or Core Idea

Interface syntax

An interface names an object shape. Here is the smallest useful form:

interface User {
  id: number;
  name: string;
}

const user: User = {
  id: 1,
  name: "Ada"
};

This says that any value of type User must have an id number and a name string.

Type alias syntax

A type alias gives a name to a type expression:

type User = {
  id: number;
  name: string;
};

const user: User = {
  id: 1,
  name: "Ada"
};

For a plain object, this looks very similar to an interface. That is why the choice is sometimes about future needs rather than immediate syntax.

Shared features

Both can describe optional and readonly properties, methods, and function types inside objects.

interface Profile {
  readonly id: string;
  nickname?: string;
  greet: () => string;
}

4. Step-by-Step Examples

Example 1: Object shape for a user record

If your type describes one object shape and is likely to grow, an interface is often the clearest choice. It reads like a contract for a data record.

interface User {
  id: number;
  name: string;
  email?: string;
}

function printUser(user: User) {
  return user.email ? user.email : user.name;
}

This example shows a straightforward object contract. An interface works naturally here because the type is an object with named properties.

Example 2: Union of values for a request state

When you need one of several possible values, a type alias is usually the right tool. Interfaces cannot directly express a union like this.

type RequestState = "idle" | "loading" | "success" | "error";

let state: RequestState = "idle";
state = "loading";

A union type like this is one of the biggest reasons to prefer type. It models a finite set of allowed values very clearly.

Example 3: Combining object shapes with an intersection

Type aliases can build new types by combining existing ones with intersections. This is useful when you want to compose capabilities.

type HasId = { id: number };
type HasName = { name: string };
type Entity = HasId & HasName;

const entity: Entity = {
  id: 42,
  name: "Widget"
};

Here, the alias makes it easy to build a new type from smaller pieces. This kind of composition is very common in larger codebases.

Example 4: Function type

A type alias can name a function type directly, which is often convenient for callbacks and utilities.

type Formatter = (value: string) => string;

const trimUpper: Formatter = (value) => value.trim().toUpperCase();

This is legal and readable with type, but not a good fit for interface unless you wrap it in an object shape.

5. Practical Use Cases

Use an interface when you are defining:

Use a type when you are defining:

A common pattern in production code is to use interfaces for long-lived object contracts and type aliases for everything else.

6. Common Mistakes

Mistake 1: Using an interface for a union type

Beginners sometimes try to make an interface behave like a union of strings or numbers. That does not work because interfaces are for object-like shapes, not literal unions.

Problem: This code tries to describe a union with an interface, which TypeScript does not allow.

interface RequestState = "idle" | "loading" | "success";

Fix: Use a type alias for unions.

type RequestState = "idle" | "loading" | "success";

The corrected version works because type can name any type expression, including unions.

Mistake 2: Expecting a type alias to declaration-merge

Interfaces can merge when you declare the same interface name more than once in compatible ways. Type aliases cannot do that, so repeated declarations produce an error.

Problem: TypeScript reports a duplicate identifier error because the same alias name is declared twice.

type User = { id: number };
type User = { name: string };

Fix: If you need repeated extension from multiple declarations, use an interface instead.

interface User { id: number ; }
interface User { name: string; }

The interface version works because TypeScript merges compatible interface declarations into one combined shape.

Mistake 3: Using an interface where a tuple or primitive alias is needed

Some types are not object maps at all. If you need a tuple, a primitive alias, or a callable type, interface is usually the wrong tool.

Problem: This code tries to model a tuple with an interface, which does not represent an indexed tuple shape correctly.

interface Point {
  [0]: number;
  [1]: number;
}

Fix: Use a tuple type alias instead.

type Point = [number, number];

The tuple alias works because type can represent ordered fixed-length arrays, which interfaces are not meant to model.

7. Best Practices

Practice 1: Use interfaces for public object contracts

If other files or packages will consume the type, interfaces often communicate intent better for object shapes. They are easy to extend and read as a contract.

interface ApiUser {
  id: string;
  displayName: string;
}

This is a good fit when the shape may evolve over time and the primary concern is describing an object.

Practice 2: Use type aliases for unions and computed types

Whenever the type is a union, intersection, or a transformation of other types, a type alias is clearer and more expressive.

type Theme = "light" | "dark";
type OptionallyLoaded<T> = T | null;

This keeps the code honest about what the type is doing instead of forcing an object-only abstraction.

Practice 3: Prefer consistency within a codebase

In many teams, the bigger issue is not which one is theoretically better, but whether the codebase uses one style consistently for the same kind of type. Consistency reduces cognitive load.

// Good: one clear convention for object contracts
interface Session {
  token: string;
  expiresAt: number;
}

// Good: one clear convention for value unions
type SessionStatus = "active" | "expired";

That split helps readers guess the shape of the type before they even open the definition.

8. Limitations and Edge Cases

A common surprise is that TypeScript types do not exist in the generated JavaScript. They help during development, but you still need runtime checks if user input might be invalid.

9. Practical Mini Project

Here is a small example that models a task tracker. It uses an interface for task objects and a type alias for allowed task states.

interface Task {
  id: number;
  title: string;
  done: boolean;
}

type TaskState = "open" | "in-progress" | "done";

const tasks: Task[] = [
  { id: 1, title: "Write docs", done: false },
  { id: 2, title: "Review PR", done: true }
];

function getState(task: Task): TaskState {
  if (task.done) {
    return "done";
  }

  return "open";
}

for (const task of tasks) {
  console.log(task.title, getState(task));
}

This tiny program shows a useful pattern: interfaces for object records, type aliases for finite value choices.

10. Key Points

11. Practice Exercise

Expected output: A string such as "Ada (admin)".

Hint: Use an interface for the object and a type alias for the union of role names.

interface User {
  id: number;
  name: string;
  email?: string;
}

type Role = "admin" | "editor" | "viewer";

function formatUser(user: User, role: Role): string {
  return `${user.name} (${role})`;
}

const result = formatUser(
  { id: 1, name: "Ada" },
  "admin"
);

console.log(result);

12. Final Summary

TypeScript interfaces and type aliases often overlap, which is why this topic confuses many developers at first. The simplest practical rule is to use interface for object contracts and type for unions, tuples, primitives, and more complex type expressions.

In real code, the best choice usually comes down to what communicates intent most clearly. Interfaces are a strong fit for long-lived object shapes, especially public APIs and class contracts. Type aliases are stronger when you need flexibility and composition. Both are useful, and most TypeScript projects end up using them together.

If you are still unsure, start with this default: define objects with interface, define everything else with type. As you read more TypeScript code, you will learn when a project benefits from one convention over the other.