TypeScript: type vs interface

stereobooster - Jul 20 '19 - - Dev Community

In one of my recent PRs I changed all interfaces to types because there were already more types than interfaces. In the review, I was asked to revert the change. I did it, but as well I wondered what the actual difference between interface and type. Let's figure out this. I use the latest TS (v3.5.1) for examples in this post.

Similarities

Records

interface IAnimal {
  name: string;
}

type Animal = {
  name: string;
};
Enter fullscreen mode Exit fullscreen mode

Generics

interface IAnimal<P = string> {
  name: P;
}

type Animal<P = string> = {
  name: P;
};
Enter fullscreen mode Exit fullscreen mode

Intersections

type Robot = {
  power: number;
};

interface IRobot {
  name: string;
}

interface IRoboAnimal1 extends IAnimal, IRobot {}
interface IRoboAnimal2 extends IAnimal, Robot {}
interface IRoboAnimal3 extends Animal, IRobot {}
interface IRoboAnimal4 extends Animal, Robot {}

type RoboAnimal1 = Animal & Robot;
type RoboAnimal2 = Animal & IRobot;
type RoboAnimal3 = IAnimal & Robot;
type RoboAnimal4 = IAnimal & IRobot;
Enter fullscreen mode Exit fullscreen mode

implements

class Dog implements IAnimal {
  name: string = "good dog";
}

class Cat implements Animal {
  name: string = "Where is my food, human?";
}
Enter fullscreen mode Exit fullscreen mode

Extend classes

class Control {
  private state: any;
}

interface ISelectableControl extends Control {
  select(): void;
}

type SelectableControl = Control & {
  select: () => void;
};
Enter fullscreen mode Exit fullscreen mode

Functions

type Bark = (x: Animal) => void;

interface iBark {
  (x: Animal): void;
}
Enter fullscreen mode Exit fullscreen mode

and generics:

type Bark = <P = Animal>(x: P) => void;

interface iBark {
  <P = Animal>(x: P): void;
}
Enter fullscreen mode Exit fullscreen mode

Recursive declarations

type Tree<P> = {
  node: P;
  leafs: Tree<P>[];
};

interface ITree<P> {
  node: P;
  leafs: ITree<P>[];
}
Enter fullscreen mode Exit fullscreen mode

Exact

type Close = { a: string };
const x: Close = { a: "a", b: "b", c: "c" };
// Type '{ a: string; b: string; c: string; }' is not assignable to type 'Close'.

interface IClose {
  a: string;
}
const y: IClose = { a: "a", b: "b", c: "c" };
// Type '{ a: string; b: string; c: string; }' is not assignable to type 'IClose'.
Enter fullscreen mode Exit fullscreen mode

Indexable

type StringRecord = {
  [index: string]: number;
};

interface IStringRecord {
  [index: string]: number;
}
Enter fullscreen mode Exit fullscreen mode

Differences

Primitive types

You can use only types to alias primitive types

type NewNumber = number;

interface INewNumber extends number {}
// 'number' only refers to a type, but is being used as a value here.

// this works
interface INewNumber extends Number {}
// but don't forget that 1 instanceof Number === false;
Enter fullscreen mode Exit fullscreen mode

Tuples

You can't declare tuples with interfaces

type Tuple = [number, number];

interface ITuple {
  0: number;
  1: number;
}

[1, 2, 3] as Tuple; // Conversion of type '[number, number, number]' to type '[number, number]' may be a mistake

[1, 2, 3] as ITuple; // Ok
Enter fullscreen mode Exit fullscreen mode

Disjoint unions

Disjoint unions works only for types:

type DomesticAnimals = { type: "Dog" } | { type: "Cat" };
Enter fullscreen mode Exit fullscreen mode

And you can't use disjoint union types with extends

interface IDomesticAnimals extends DomesticAnimals {}
// An interface can only extend an object type or intersection of object types with statically known members
Enter fullscreen mode Exit fullscreen mode

new

You can declare the type of new

interface IClassyAnimal {
  new (name: string);
}
Enter fullscreen mode Exit fullscreen mode

it doesn't work as you expect

class Parrot implements IClassyAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
//  Type 'Parrot' provides no match for the signature 'new (name: string): void'.
Enter fullscreen mode Exit fullscreen mode

constructor doesn't seem to work either

interface IClassyAnimal {
  constructor(name: string): void;
}

class Parrot implements IClassyAnimal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}
// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
//  Types of property 'constructor' are incompatible.
//    Type 'Function' is not assignable to type '(name: string) => void'.
//      Type 'Function' provides no match for the signature '(name: string): void'.
Enter fullscreen mode Exit fullscreen mode

Only one declaration per scope

You can declare types only once per scope

type Once = { a: string };
type Once = { b: string };
// Duplicate identifier 'Once'.
Enter fullscreen mode Exit fullscreen mode

you can declare interface more than once per scope (the final result will be the sum of all declarations)

interface IOnce {
  a: string;
}
interface IOnce {
  b: string;
}
Enter fullscreen mode Exit fullscreen mode

Utility types

Most of the time you would use types instead of interfaces to create utility types, for example:

export type NonUndefined<A> = A extends undefined ? never : A;
Enter fullscreen mode Exit fullscreen mode

Conclusion

Not all of those things were possible in early versions of TS, so people got used to interfaces. But in the latest version of TS, it seems that types are more capable and we can always use them 🤔. Or I miss something?

There are a lot of nuances in TS - something may work for a small example (which I showed), but broken for big ones. Please correct me if I missed something.

Dedicated to @thekitze.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .