Back to blog
Oct 31, 2024
8 min read

Type vs Interface: What to Choose?

Highlighting key differences and best use cases

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

  1. For type - after the name, we use an equals sign, whereas for interface we don’t:
type Animal = {
     name: string
}

interface Animal {
    name: string
}
  1. To extend a type, we use an ampersand (in TypeScript, it’s called an intersection) — in other words, we create an intersection type. To extend an interface, we use the familiar OOP keyword extends.
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:

Playground

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:

Playground

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.

Playground

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 a type.
  • A class can implement both an interface and a type.

Conclusions

Among the significant differences between interfaces and type aliases, there are two decisive factors to pay attention to:

  1. `Interfaces` have an advantage in terms of performance.
  2. 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 from type.

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, use type :)

Thank you for your attention!