Type Aliases and Interfaces

Introduction

In the beginning, types aliases were quite different from interfaces.

Over the years, forum discussions and posts would happen from time to time about the differences, pros and cons of each one. As new versions of TypeScript would be released, those posts and pieces of advice would become outdated.

As of TypeScript 4 (maybe earlier), there is not much difference between interfaces and type aliases. What you can do with one, you can do with the other (albeit with different approaches).

The main difference as of TypeScript 5.x is that interfaces allow for declaration merging.

Example Interface vs Type Alias

interface IPerson {
  id: number;
  name: string;
};

//                 <1>
interface IStudent extends IPerson {
  email: string;
}

const student1: IStudent = {
  id: 1,
  name: "Aayla Secura",
  email: "aayla@jedischool.dev",
};

type TPerson = {
  id: number;
  name: string;
};

//                     <2>
type TStudent = TPerson & { email: string };

const student2: TStudent = {
  id: 2,
  name: "Ahsoka Tano",
  email: "ahsoka@jedischool.dev",
};
  1. Using extends to create another interface from a base interface.

  2. Using the intersection operator & to compose together the two types.

Note

The intersection behaves more like an union operation this example. Read more on Intersection Types and Union Types.

Interface Declaration Merging

Interfaces allow for the so called declaration merging. It means an interface can be reopen and new things can be added to it.

interface Page {
  title: string;
  url: string;
}

interface Page {
  locale: "es_MX" | "hi_IN" | "pt_BR";
}

The above is same as if we had done this instead:

interface Page {
  title: string;
  url: string;
  locale: "es_MX" | "hi_IN" | "pt_BR";
}

It is useful feature for when we need to augment an interface defined elsewhere outside our control (maybe from a library or a global type like Window):

////
// Augments Window interface by opening it and adding information
// about analytics.fireEvent().
//
interface Window {
  analytics: {
    fireEvent: (name: string, payload: unknown) => void;
  };
}

////
// Possible to call analytics.fireEvent() because we augmented the
// Window interface using declaration merging and made it aware
// that there is this new analytics object which provides this
// fireEvent() method.
//
window.analytics.fireEvent("user-click", { id: 1, name: "Jon Doe" });

////
// Other things not part of the default Window interface and not
// added to the types in some for is not legal.
//
window.datalayer.fireEvent("user-click", { id: 1 });
//     ~~~~~~~~
//         \
//          \
//           +---> Type error. Window knows nothing
//                 about this datalayer thing. Thus,
//                 it doesn't type check.
////

Declaration Merging Danger

One problem with declaration merging is that (at least currently, as of TypeScript 5.4.3), the type checker doesn’t inform us when we are (maybe accidentally?) augmenting an interface.

Suppose we have a form to collect certain data and we want to represent its fields using an interface:

interface FormData {
  name: string;
  email: string;
  age: number;
}

function sendData(data: FormData): void {
  ////
  // Seems to work fine and type-checks
  // but FAILS AT RUNTIME.
  //
  log(data.values());
  //
  // Where is .values() coming from‽
  ////
}

FormData interface merging

name, email and age are expected to be in a value of type FormData, but where are the other properties coming from?

Gotcha! Because there exists an interface called FormData in the DOM, we ended up not creating an interface named FormData, but actually augmenting (performing interface merging) on the existing FormData interface.

And things get worse! It fails at runtime as the value data actually only contains the properties name, email and age.

Had we used a type alias instead, the problem would have been immediately apparent.The type checker would complain we were trying to redeclare an existing type with a message something like “Duplicate identifier ‘FormData’.”

In short, it is a good idea to try to stick to type aliases for our own modules and things we control so we don’t fall into the sort of problem just described, and use use interfaces when it is expected that other consumers will augment them through declaration merging (and only when augmenting through intersection don’t seem like a good solution for a given problem).