If you’ve worked with TypeScript, you’ve likely wondered whether it’s better to use type
or interface
for defining types. Let’s break it down and find out.
Only type
 in certain cases
First of all, it’s worth noting that the choice between type
and interface
is irrelevant when it comes to types where using type
is our only option.
Types
The type
keyword is used to create type aliases. This means that we can create aliases for any type, including:
Literal Types:
type LuckyNumber = 13;
type True = true;
type HeWhoMustNotBeNamed = 'Voldemort';
Primitive Types:
type Name = string;
type Choice = boolean;
Tuples:
type MagicalCreatureSighting = [creatureName: string, location: string, isDangerous: boolean, numberSeen: number];
Union Types:
type PowerfulWizard = "Albus Dumbledore" | HeWhoMustNotBeNamed;
Mapped Types:
type MyPartial<T> = { [P in keyof T]?: T[P]; };
Functions:
type CastSpell = (spell: "Expelliarmus" | "Lumos" | "Alohomora", target: string) => void;
Objects:
type Wizard = {
name: string;
house: 'Gryffindor' | 'Hufflepuff' | 'Ravenclaw' | 'Slytherin';
age: number;
knownSpells: string[];
pet?: 'Owl' | 'Cat' | 'Toad';
}
So with type
, we can define any type, hence they are called type aliases.
Interfaces
Interfaces in TypeScript are mainly used to define the structure of objects and functions.
So, the choice between type
and interface
really only comes up when working with objects or functions. For anything else, type
is the way to go.
Functions
When it comes to functions, the choice between type
and interface
is usually straightforward since most developers prefer using type aliases for their clearer and more readable syntax.
For comparison:
type CastSpell = (spell: "Expelliarmus" | "Lumos" | "Alohomora", target: string) => void;
interface CastSpell {
(spell: "Expelliarmus" | "Lumos" | "Alohomora", target: string): void;
}
As you can see, the type
syntax for functions is simpler and easier to read.
Objects
For objects, opinions can vary, and aside from syntax, there are some significant differences to consider. Let’s start with the syntax.
Syntax
- For
type
- after the name, we use an equals sign, whereas forinterface
we don’t:
type Animal = {
name: string
}
interface Animal {
name: string
}
- To extend a
type
, we use an ampersand (in TypeScript, it’s called anintersection
) — in other words, we create an intersection type. To extend aninterface
, we use the familiar OOP keywordextends
.
type Goat = Animal & {
isSmart: true
}
interface Sheep extends Animal {
isSmart: false
isCute: true
}
Performance
After understanding the syntax, let’s look at more substantial differences, starting with those that can be found in the official documentation in TypeScript > Handbook > Everyday Types.
Let’s start with performance:
Using interfaces with extends can often be more performant for the compiler than type aliases with intersections.
In a link on how to optimize TypeScript performance, the first recommendation is to use interface extends instead of type intersections.
So what does this performance difference mean in practice, and when can it be noticeable?
Firstly, it’s important to understand that this isn’t the kind of performance that affects the end-user experience of your product (like an app or website).
This is the speed at which your TS code is compiled. The negative impact on performance here refers to an increase in the time taken for type checking and building artifacts (files like .js and .d.ts).
This type of performance impacts the developer experience (DX), and the biggest difference can be felt, for example, if you have a large project that needs to be built regularly (such as part of the CI process), which involves compiling all the TS code.
For example, in this repository, the compilation time of 10,000 interfaces using extends was compared to 10,000 type intersections. The result was that the interface variant took 1 min 26 sec 629 ms, while the intersections variant took 2 min 33 sec 117 ms.
Declaration Merging
Another interesting difference between types
and interfaces
is what is called Declaration Merging, which is unique to `interfaces`. In a basic example, it works like this: if you declare interfaces with the same name multiple times, TypeScript will automatically merge them into one interface.
interface Animal {
species: string;
sound: string;
}
interface Animal {
favoriteFood: string;
canFly: boolean;
}
const cat: Animal = {
species: 'Cat',
sound: 'Meow',
favoriteFood: 'Fish',
canFly: false // Not yet, but we're working on it!
}; // OK
We won’t go into detail about why this feature might be useful or how to use it — we’ll save that for another article. For most day-to-day tasks, though, declaration merging can lead to unwanted errors, like accidentally declaring the same interface twice, resulting in an interface that has properties from both declarations.
It’s probably worth noting that I haven’t had this happen to me in practice, even though I use interfaces quite intensively.
Errors
Another difference is that interface with extends handles errors better than type with intersection. This can also be read about in TypeScript > Handbook > Objects.
Let’s consider the following example:
Suppose we have interface Person
:
interface Person {
name: string;
age: string;
}
If we try to extend the Person
interface but change any property type to an incompatible type, we will get the following error:
interface IPerson extends Person {
// ~~~~~~~ Interface 'IPerson' incorrectly extends interface 'Person'.
// Types of property 'age' are incompatible.
// Type 'number' is not assignable to type 'string'.
age: number;
}
If we do the same action with types and intersection, we won’t get an error. Instead, the age
property will be assigned the type never
.
type TPerson = Person & { age: number; }; // no error, unusable type
In this case, the behavior of interface
is more desirable, as it helps us catch errors sooner.
Index Signature
Another minor difference is that type aliases have an implicit index signature, whereas interfaces do not. This is better shown in an example:
interface Animal {
name: 'some animal'
}
declare const animal: Animal;
const handleRecord = (obj: Record<string, string>) => { }
const result = handleRecord(animal)
// ~~~~~~~
// Argument of type 'Animal' is not assignable to parameter of type 'Record<string, string>'.
// index signature for type 'string' is missing in type 'Animal'.(2345)
In this example, handleRecord(animal)
will give us an error.
The error occurs because interfaces can be extended via declaration merging, and there’s no guarantee that new properties of `Animal*`* will match Record<string,string>
.
To fix this, we either need to explicitly add an index signature to Animal
, such as:
interface Animal {
name: 'some animal'
[key: string]: string
}
or simply replace interface
with type
:
type Animal = {
name: 'some animal'
}
Error Messages
Another very small difference is in how errors are shown, but this difference is so minor that you might not notice it at first glance.
Similarities
That’s all for the differences. In every other aspect, when it comes to objects, interface
and type
behave the same way. For example:
- They can be generics.
- An
interface
can extend atype
. - A class can implement both an
interface
and atype
.
Conclusions
Among the significant differences between interfaces and type aliases, there are two decisive factors to pay attention to:
- `Interfaces` have an advantage in terms of performance.
- On the other hand, they have the declaration merging mechanism, which can lead to unexpected errors.
All other differences, in my opinion, are not as significant.
The official documentation suggests using interface
by default, and switching to type
only if you need features like mapped types or union types.
For the most part, you can choose based on personal preference, and TypeScript will tell you if it needs something to be the other kind of declaration. If you would like a heuristic, use
interface
until you need to use features fromtype
.
Many articles from different authors on the internet recommend always using type
to be consistent and avoid the risk of accidentally extending interfaces via declaration merging.
I would suggest the following approach:
- If you are working on a legacy project, use what is commonly used in that project.
- If it’s a new project, use what you prefer, as even the “significant” differences are not that significant.
- If you have no preference, and if it’s going to be a fairly large project where TypeScript compilation time will be important, then favor
interface
; otherwise, usetype
:)
- If you have no preference, and if it’s going to be a fairly large project where TypeScript compilation time will be important, then favor
Thank you for your attention!