TypeScript: type vs interface

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

1interface IAnimal {
2  name: string;
3}
4
5type Animal = {
6  name: string;
7};

Generics

1interface IAnimal<P = string> {
2  name: P;
3}
4
5type Animal<P = string> = {
6  name: P;
7};

Intersections

 1type Robot = {
 2  power: number;
 3};
 4
 5interface IRobot {
 6  name: string;
 7}
 8
 9interface IRoboAnimal1 extends IAnimal, IRobot {}
10interface IRoboAnimal2 extends IAnimal, Robot {}
11interface IRoboAnimal3 extends Animal, IRobot {}
12interface IRoboAnimal4 extends Animal, Robot {}
13
14type RoboAnimal1 = Animal & Robot;
15type RoboAnimal2 = Animal & IRobot;
16type RoboAnimal3 = IAnimal & Robot;
17type RoboAnimal4 = IAnimal & IRobot;

implements

1class Dog implements IAnimal {
2  name: string = "good dog";
3}
4
5class Cat implements Animal {
6  name: string = "Where is my food, human?";
7}

Extend classes

 1class Control {
 2  private state: any;
 3}
 4
 5interface ISelectableControl extends Control {
 6  select(): void;
 7}
 8
 9type SelectableControl = Control & {
10  select: () => void;
11};

Functions

1type Bark = (x: Animal) => void;
2
3interface iBark {
4  (x: Animal): void;
5}

and generics:

1type Bark = <P = Animal>(x: P) => void;
2
3interface iBark {
4  <P = Animal>(x: P): void;
5}

Recursive declarations

1type Tree<P> = {
2  node: P;
3  leafs: Tree<P>[];
4};
5
6interface ITree<P> {
7  node: P;
8  leafs: ITree<P>[];
9}

Exact

1type Close = { a: string };
2const x: Close = { a: "a", b: "b", c: "c" };
3// Type '{ a: string; b: string; c: string; }' is not assignable to type 'Close'.
4
5interface IClose {
6  a: string;
7}
8const y: IClose = { a: "a", b: "b", c: "c" };
9// Type '{ a: string; b: string; c: string; }' is not assignable to type 'IClose'.

Indexable

1type StringRecord = {
2  [index: string]: number;
3};
4
5interface IStringRecord {
6  [index: string]: number;
7}

Differences

Primitive types

You can use only types to alias primitive types

1type NewNumber = number;
2
3interface INewNumber extends number {}
4// 'number' only refers to a type, but is being used as a value here.
5
6// this works
7interface INewNumber extends Number {}
8// but don't forget that 1 instanceof Number === false;

Tuples

You can’t declare tuples with interfaces

 1type Tuple = [number, number];
 2
 3interface ITuple {
 4  0: number;
 5  1: number;
 6}
 7
 8[1, 2, 3] as Tuple; // Conversion of type '[number, number, number]' to type '[number, number]' may be a mistake
 9
10[1, 2, 3] as ITuple; // Ok

Disjoint unions

Disjoint unions works only for types:

1type DomesticAnimals = { type: "Dog" } | { type: "Cat" };

And you can’t use disjoint union types with extends

1interface IDomesticAnimals extends DomesticAnimals {}
2// An interface can only extend an object type or intersection of object types with statically known members

new

You can declare the type of new

1interface IClassyAnimal {
2  new (name: string);
3}

it doesn’t work as you expect

1class Parrot implements IClassyAnimal {
2  name: string;
3  constructor(name: string) {
4    this.name = name;
5  }
6}
7// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
8//  Type 'Parrot' provides no match for the signature 'new (name: string): void'.

constructor doesn’t seem to work either

 1interface IClassyAnimal {
 2  constructor(name: string): void;
 3}
 4
 5class Parrot implements IClassyAnimal {
 6  name: string;
 7  constructor(name: string) {
 8    this.name = name;
 9  }
10}
11// Class 'Parrot' incorrectly implements interface 'IClassyAnimal'.
12//  Types of property 'constructor' are incompatible.
13//    Type 'Function' is not assignable to type '(name: string) => void'.
14//      Type 'Function' provides no match for the signature '(name: string): void'.

Only one declaration per scope

You can declare types only once per scope

1type Once = { a: string };
2type Once = { b: string };
3// Duplicate identifier 'Once'.

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

1interface IOnce {
2  a: string;
3}
4interface IOnce {
5  b: string;
6}

Utility types

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

1export type NonUndefined<A> = A extends undefined ? never : A;

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.

Except where otherwise noted, content on this site is licensed under Creative Commons Attribution-NonCommercial-ShareAlike 4.0