Why do I prefer Types over Interfaces in TypeScript? A Comprehensive Guide
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 calledReadonly
that takes a generic parameterT
. This meansReadonly
can be applied to any type, andT
represents that type.readonly
— Thereadonly
modifier before[P in keyof T]
means that for each propertyP
, 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 typeT
. The expression[P in keyof T]
means that we will iterate over each property keyP
inT
.T[P]
— This refers to the type of each propertyP
in the typeT
. For example, ifT
is an object type with a propertyname
of typestring
,T[P]
would bestring
for the propertyname
.
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 namedNonNullable
, which is a generic type that accepts a typeT
. This means thatNonNullable
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 typeT
is eithernull
orundefined
, then the condition evaluates totrue
. In that case, the type would be replaced by the second part of the conditional (never
). Whilenull | undefined
is a union type, meaning that we are checking whetherT
is eithernull
orundefined
.? never : T
— The conditional type is structured as:T extends null | undefined ? never : T
.- If
T
isnull
orundefined
, then the type evaluates tonever
. Otherwise, the type remains asT
. 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
andundefined
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 includesstring
,null
, andundefined
.NonNullable<MyType>
will removenull
andundefined
fromMyType
, leaving onlystring
.- As a result,
NonNullableMyType
is effectively juststring
, and attempting to assignnull
orundefined
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 typeStatus
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 typeStatus
, so it can only hold the values"success"
or"error"
. - Assigning
"pending"
or any other value toresult
will result in a TypeScript error, because"pending"
is not part of theStatus
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! 😊