typescript
February 1, 2020

The difference between type and interface in TypeScript

Type and interface in TypeScript often confuse people because they look similar on the surface. The situation gets worse with outdated articles, biased comparisons, and style guides from some frameworks. For example, Angular has the tslint rule interface-over-type-literal enabled by default, which forces you to use interfaces instead of types wherever possible. In this article, we'll look at the difference between type and interface in TypeScript and figure out what you should actually use.

Similarities

Interfaces and types can be used to describe data structures:

type Employee = {
  salary: number;
}

// Or
interface Employee {
  salary: number;
}

Can be used for typing functions:

interface CalculateSalary {
  (employee: Employee): number; 
}

// Or

type CalculateSalary = (employee: Employee) => number;

Interfaces and types can be implemented by classes:

type Employee = {
  giveEstimate(task: Task): number;
}

// Or

interface Employee {
  giveEstimate(task: Task): number;
}

class YoungDeveloper implements Employee {
  giveEstimate(task: Task): number {
    return task.complexity * 0.01;
  }
}

class MatureDeveloper implements Employee {
  giveEstimate(task: Task): number {
    return task.complexity * random(10, 1000) * Math.PI;
  }
}

Interfaces and types allow expressing type intersections:

type TwitterProfile = Photographer & Musician & Entrepreneur & CoffeeDrinker;

// Or

interface TwitterProfile extends Photographer, Musician, Entrepreneur, CoffeeDrinker {};

Difference 1 - Mapped Types

Interfaces cannot be combined with mapped types (Required, Pick, Readonly, Partial, and others):

// Works with type
type RealProfile = Pick<TwitterProfile, 'drinkCoffee'>;

// Doesn't work with interface
interface RealProfile extends Pick<TwitterProfile, 'drinkCoffee'> {};

An interface can only extend interfaces, classes, or other types, so the code needs to be rewritten like this:

type OnlyDrinksCoffee = Pick<TwitterProfile, 'drinkCoffee'>;

interface RealProfile extends OnlyDrinksCoffee {}

For the same reason, only with a type can you require that all properties be mandatory or conversely optional::

type TraineeDeveloper = Partial<{
  salary: number;
  sleep: boolean;
  eat: boolean;
}>

// Can exist without salary, food, and sleep
const trainee: TraineeDeveloper = {} 

Difference 2 - Union

Interfaces allow expressing type intersections, but don't allow expressing unions. Example of a constraint with types that's unrealizable with interfaces::

type Wish = 
  | { fast: true, quality: true, cheap: false } // Expensive
  | { fast: true, quality: false, cheap: true } // Poor quality
  | { fast: false, quality: true, cheap: true } // Slow 
  
// Won't compile
const wish: Wish = { fast: true, quality: true, cheap: true }

Difference 3 - Declaration merging

Interfaces support declaration merging - merging interfaces with the same names:

interface Employee {
  salary: number;
}

interface Employee {
  age: number;
}

// Compilation error, as salary wasn't provided
const employee: Employee = { age: 23 };

This feature can be used if third-party library typings are outdated and you need to extend the interface with missing properties and methods. If you're making your own library, then it's up to you whether to provide such extension points. If the library is in TypeScript - then you shouldn't, as the type declarations in the output will always be up-to-date and users won't need to fix discrepancies between types and runtime. Nowadays, more and more libraries are written directly in TypeScript or come with typings, so the need to use declaration merging arises less frequently. Additionally, not everything can be fixed in an outdated interface - you can't remove a property or change the type of an existing one::

interface Employee {
  salary: number;
}

interface Employee {
  salary?: number;
}

// Error, an employee still demands salary
const employee: Employee = {};

Difference 4 - Recursive types

Before TypeScript 3.7, there were differences in how recursive types and interfaces worked. With types, you couldn't type recursive structures, but in newer versions of the language, there's no problem. Details in the language release notes. Check here for more details.

Difference 5 - Compatibility with Record Type

Because interface supports declaration merging (can be extended anywhere), it cannot be used where Record<string, any> is expected. This can be a problem in situations where you need to use URLSearchParams or other browser APIs expecting Record<string, any>:

interface Employee {
  name: string;
}

const employee: Employee = { name: '' }

new URLSearchParams(employee);

This code won't compile with an interface, but will work if you change the interface to a type.

Conclusion

Types are the more preferable option, as you can replace interfaces with types, but not vice versa. For using TypeScript's advanced functionality - mapped types, union types, and conditional types - interfaces won't work.