Sitemap

Why do I prefer Types over Interfaces in TypeScript? A Comprehensive Guide

8 min readMar 22, 2025

Introduction

TypeScript gives developers two powerful tools for defining the shape of objects: types and interfaces. While both serve similar purposes, they have distinct differences that can affect code maintainability, flexibility, and readability.

Over time, I’ve found myself gravitating toward types rather than interfaces for most of my projects. While interfaces have their advantages — especially when working with class-based inheritance — types provide a more flexible and consistent experience in many scenarios.

In this article, I’ll break down why I prefer types over interfaces, highlighting key differences, real-world use cases, and common pitfalls to avoid. If you’ve ever been unsure which one to use, this might just help you make a more informed decision.

1. Primitive Types

Types can define aliases for primitive types (e.g., string, number, boolean), whereas interfaces cannot.

type Name = string;
type Pi = number;
type IsActive = boolean;

2. Types Are More Versatile

One of the main reasons I prefer types is their versatility. Types can be used for much more than just defining object structures — they can represent unions, intersections, primitive types, function signatures, and even mapped types. With types, you can define complex structures easily.

a. Union Types

Types can represent union (|) and intersection (&) types, which are not possible with interfaces.

type ID = string | number;

// Real life example
type Status = "success" | "error" | "loading";

b. Intersection Types

type Combined = TypeA & TypeB;

// Real life example
type User = {
id: number;
name: string;
email?: string;
};

type Admin = User & { role: string };

// Final shape of Admin type

// id: number;
// name: string;
// email: string | undefined;
// role: string;

With interfaces, you can’t define union types like "success" | "error" | "loading", making types the more powerful option in many cases.

3. Types Allow More Flexible Composition

Interfaces support declaration merging, which allows multiple interface declarations to merge into one. While this can be useful, it can also lead to unintended behaviors, especially in large codebases.

Types, on the other hand, use explicit composition, making the code more predictable and easier to understand.

a. Types composition

// Using types for composition
type Animal = { name: string };
type Dog = Animal & { breed: string };

const myDog: Dog = { name: "Buddy", breed: "Golden Retriever" };

b. Interface composition

With interfaces, merging can sometimes lead to unexpected issues:

interface Animal {
name: string;
}

// Merging changes the structure
interface Animal {
age: number;
}

// This works, but can be confusing
const cat: Animal = { name: "Whiskers", age: 3 };

While interface merging can be useful in some scenarios (like augmenting third-party libraries), it can also lead to unexpected redefinitions that break code.

4. Types Work Better with Complex Data Structures

When dealing with complex structures like tuples, unions, or mapped types, types are simply more expressive.

a. Tuples

Types can define tuple types, which are fixed-length arrays with specific types for each element. Interfaces cannot represent tuples directly.

type coords = [number, number];

b. Mapped Types

Types can be used to create mapped types, which transform the properties of an existing type. This is not possible with interfaces.

type Readonly<T> = {
readonly [P in keyof T]: T[P];
};

Let’s take a moment and break this code into pieces:

  • type Readonly<T> — This declares a type alias called Readonly that takes a generic parameter T. This means Readonly can be applied to any type, and T represents that type.
  • readonly — The readonly modifier before [P in keyof T] means that for each property P, the property becomes read-only. This means once an object of this type is created, its properties cannot be modified.
  • [P in keyof T] — This part uses mapped types in TypeScript. keyof T gives us a union of all the keys (property names) of type T. The expression [P in keyof T] means that we will iterate over each property key P in T.
  • T[P] — This refers to the type of each property P in the type T. For example, if T is an object type with a property name of type string, T[P] would be string for the property name.

How this works ?

type PersonType = {
name: string;
age: number;
};

type ReadonlyPerson = Readonly<PersonType>;

const person: ReadonlyPerson = {
name: "John",
age: 30,
};

// The following lines would result in a TypeScript error, because properties are read-only

// Error: Cannot assign to 'name' because it is a read-only property.
// obj.name = "Jane";

// Error: Cannot assign to 'age' because it is a read-only property.
// obj.age = 25;

In example above, Readonly<PersonType> creates a type ReadonlyPerson where both name and age are read-only. Therefore, you cannot change their values after the object has been created.

Simpler example

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

// { id?: number; name?: string; email?: string; }
type PartialUser = Optional<User>;

Interfaces don’t support these features. You’d need to rely on utility types to achieve similar functionality, which adds unnecessary complexity.

5. Conditional Types

Types support conditional logic, allowing you to create types that depend on conditions. Interfaces do not support this.

type NonNullable<T> = T extends null | undefined ? never : T;

For the code above:

  • type NonNullable<T> — This declares a type alias named NonNullable, which is a generic type that accepts a type T. This means that NonNullable will work with any type you pass into it.
  • T extends null | undefined — This is a conditional type check. T extends null | undefined means that if the type T is either null or undefined, then the condition evaluates to true. In that case, the type would be replaced by the second part of the conditional (never). While null | undefined is a union type, meaning that we are checking whether T is either null or undefined.
  • ? never : T — The conditional type is structured as: T extends null | undefined ? never : T.
  • If T is null or undefined, then the type evaluates to never. Otherwise, the type remains as T.
  • never is a special type in TypeScript that represents values that will never occur (e.g., values that are impossible or unreachable). It is used here to effectively "remove" null and undefined from the type.
type MyType = string | null | undefined;

// NonNullableMyType will be equivalent to "string" (null and undefined are removed)
type NonNullableMyType = NonNullable<MyType>;

const value1: NonNullableMyType = "Hello"; // OK
const value2: NonNullableMyType = null; // Error: Type 'null' is not assignable to type 'string'.
const value3: NonNullableMyType = undefined; // Error: Type 'undefined' is not assignable to type 'string'.
  • MyType is a union type that includes string, null, and undefined.
  • NonNullable<MyType> will remove null and undefined from MyType, leaving only string.
  • As a result, NonNullableMyType is effectively just string, and attempting to assign null or undefined will result in an error.

6. Literal Types

Types can define literal types (e.g., specific strings or numbers), which are useful for defining exact values.

type Status = "success" | "error";

Defines a type alias called Status that can take one of two specific string values: "success" or "error". Let's break this down:

a. type status:

  • Status is the name of the type alias being defined. Type aliases in TypeScript allow you to create a custom name for a type or a union of types.

b. “success” | “error”:

  • This part defines a union type. The | (pipe) symbol is used to create a union, meaning that the type Status can be either "success" or "error".
  • The string values "success" and "error" are literal types in TypeScript, meaning that only these exact string values are allowed.

The Status type can only be assigned one of the two literal values: "success" or "error". This ensures that any variable of type Status must be either "success" or "error", and no other values are allowed.

type Status = "success" | "error";

let result: Status;

result = "success"; // OK
result = "error"; // OK
result = "pending"; // Error: Type '"pending"' is not assignable to type 'Status'.
  • The variable result is declared with the type Status, so it can only hold the values "success" or "error".
  • Assigning "pending" or any other value to result will result in a TypeScript error, because "pending" is not part of the Status type.

7. Types Are More Consistent in Modern Codebases

Many modern TypeScript codebases prefer types over interfaces because:

  • Types work better with React’s TypeScript patterns (e.g., defining props and state).
  • They align better with functional programming paradigms, especially when working with utility types.
  • They provide a more predictable developer experience without worrying about interface merging.

Many teams adopt a “use types unless an interface is necessary” approach, which simplifies TypeScript usage across the project.

8. When to Use Interfaces Instead

Despite my preference for types, interfaces do have their place. You should still use interfaces if:

You are working with class-based object-oriented programming

  • Interfaces integrate well with classes and can be implemented directly.

You need to merge declarations (e.g., extending third-party types)

  • Useful for augmenting library types without modifying the source code.

You are designing a public API for libraries

  • Some TypeScript documentation suggests interfaces for public APIs because they allow merging.

For example, an interface is useful in this scenario:

interface Animal {
name: string;
}

class Dog implements Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}

However, in most modern TypeScript projects — especially React or functional-heavy codebases — types provide a more flexible and intuitive experience.

9. Final Thoughts: Why I Prefer Types

At the end of the day, the choice between types and interfaces depends on your project’s needs. But for most modern TypeScript applications, types offer greater flexibility, more expressive compositions, and fewer unexpected behaviors compared to interfaces.

To summarize:

✔️ Use types when you need unions, intersections, tuples, or mapped types.
✔️ Use types for consistent, predictable structures in functional programming.
✔️ Use interfaces only when working with class-based inheritance or declaration merging.

Ultimately, both are valid tools in TypeScript — but in my experience, types provide a more seamless and powerful way to define structures in modern development.

I hope this article helped clarify why types often work better than interfaces in modern TypeScript development. If you have a different perspective, I’d love to hear it!

💬 Do you prefer types or interfaces? Let me know in the comments, and let’s discuss! 🚀

If you found this helpful, consider liking, sharing, or following for more TypeScript and JavaScript content. Happy coding guys! 😊

--

--

Najm Eddine Zaga
Najm Eddine Zaga

Written by Najm Eddine Zaga

🇹🇳 | Software Developer & Javascript enthusiastic | TypeScript | ReactJs | NextJs | NestJs | ExpressJs | Redux

No responses yet