Interface vs Type in TypeScript

Emilien Jegou, Wed June 18 2025

TypeScript allows us to use interface and type interchangeably. We tend to favor type aliases because they offer a broader and more powerful set of features. For instance, both can express inheritance, but only type lets you perform advanced type transformations such as Partial<T>, Omit<T, K>, and other mapped types.

It seems logical that when two constructs are so similar, we’d choose the more capable one—especially when its syntax is often more concise. Features traditionally associated with interfaces, such as inheritance and class interoperability, are now handled just as easily (and sometimes more elegantly) with type aliases in modern TypeScript.

So, should we only use type? Not quite. There are still a few specific capabilities that only interfaces provide—which is exactly what this article explores. We won’t rehash everything type can do; instead, we’ll focus on the small but important subset of features where interfaces still have the edge.

Interfaces are almost a subset of types

"This" keyword

Using interfaces allow the use of the this keyword which can help shorten methods chaining and improve clarity e.g.:

typescript
interface MyChain<A, B, C, D> {
   chainA(a: A): this;
   chainB(b: B): this;
   chainC(b: C): this;
   chainD(b: D): this;
   execute(): My;
}

// You cannot use the "this" keywords in type.
type MyChain<A, B, C, D> = {
   chainA(a: A): MyChain<A, B, C, D>;
   chainB(b: B): MyChain<A, B, C, D>;
   chainC(b: C): MyChain<A, B, C, D>;
   chainD(b: D): MyChain<A, B, C, D>;
   execute(): My;
}

Declaration merging

Declaration merging (or interface augmentation) is a feature that allow multiple interface in the same scope to "merge" implicitely, e.g.:

typescript
interface MyInterface {
    age: number;
}

interface MyInterface {
    name: string;
}

const x: MyInterface = { age: 123, name: 'John Doe' }

This is a feature that is generally avoided by developers for a few reason:

  • it doesn’t work well with LSP or typescript (e.g. goto definition)
  • it makes your code harder to navigate
  • Cognitively it's confusing and bug-inducing

Yet we may still find use-cases for declaration merging, for example when trying to extends external interfaces.

On Typescript default

Declaration merging can be used to extends global types in your application:

typescript
interface Window {
  myAppVersion: string;
}

window.myAppVersion = "1.2.3";

This is the how the library ts-reset improve javascript standard library type-safety, a single import can avoid many typescript pitfall; this is only possible through declaration merging:

typescript
import '@total-typescript/ts-reset'

On external libraries

The same principles is often found in module files for extending external libraries, express is a good example for it:

typescript
// global.d.ts or in a module augmentation block
declare module 'express' {
  interface Request {
    user?: { id: string; role: string };
  }
}

If you declare your external interfaces as type's in your application 3rd party user will not be able to extend them in the same manner.

If your package has weak typing (e.g. overly resort to “any” or “unknown” on external interface). Your users could then resort to declaration merging for covering your type declaration as a palliative solution.

Implicit index signatures

type aliases have an implicit index signature, but interface don't, it's a subtle difference that could lead to errors e.g.:

typescript
type DemoType = { hello: number };

const t : { [k: number]: string } = { hello: 123 } as DemoType;

// vs

interface DemoInterface { hello: number }

// Type 'DemoInterface' is not assignable to type '{ [k: number]: string; }'.
const i : { [k: number]: string } = { hello: 123 } as DemoInterface;

The implicit type signature on type actually create an invalid casting: DemoType doesn't have any numeric keys or string values. this is sort of a quirky scenario but worth mentionning, I actually prefer how interfaces are stricter by default on this particular case.

Worth noting that class will behave in the same way as interfaces even when they are extending a type.

Difference in error types

One of the main cited reason for the use of interface are "better transpilation messages".

Yet my testing didn't see significant differences, even on the official documentation example; I am leaving this one up to interpretation, depending on your typescript engine you may get different result, so keep an eye on it.

typescript
type BirdType = { wings: 2; };
interface BirdInterface { wings: 2; }

type ChickenType = { eggColor: 'white'; } & BirdType;
interface ChickenInterface extends BirdInterface { eggColor: 'white'; }

let ct1: ChickenType = { eggColor: 'white' };
// Type '{ eggColor: "white"; }' is not assignable to type 'ChickenType'.
// Property 'wings' is missing in type '{ eggColor: "white"; }' but required in type 'BirdType'.
//
// type BirdType = { wings: 2; };
//                   ~~~~~
// 'wings' is declared here.


let ci2: ChickenInterface = { eggColor: 'white' };
// Property 'wings' is missing in type '{ eggColor: "white"; }' but required in type 'ChickenInterface'.
//
// interface BirdInterface { wings: 2; }
//                           ~~~~~
// 'wings' is declared here.

Conclusion

You can assume that most of the time, interfaces are used as a "force of habit" rather than any specific need. Either from OOP use, or simply from recommendation that ceased to be relevant.

The most important thing — whether you choose type or interface — is to understand the features you are missing, luckily for you that list is extremely thin for interfaces, outside of declaration merging, type offers a more powerful and flexible foundation for most use cases in today’s TypeScript.