Types vs Interfaces in TypeScript

April 23, 2020

The line that defines when you should be using a TypeScript type or an interface is one that often gets blurred. Let's go over the differences between the two and when you should be using one over the other.

So, what are the differences?

Let me preface this with clarifying that we're talking about type aliases, and not a type literal like any for example.

You can think of an interface as a virtual structure in which it defines the properties and keys of an object, and can also contain methods and nested objects within it as well. Types are similar in this regard, minus a few subtle differences.

Let's write some code and break them down:

1. Classes cannot implement types

class Point {
  x: number
  y: number
}
interface Shape {
  area: () => number
}
type Perimeter = {
  perimeter: () => number
}
// this shape can either be of type Shape and Point
// or Perimeter and Point
type RectangleShape = (Shape | Perimeter) & Point
class Rectangle implements RectangleShape {
  x = 2
  y = 3
  area() {
    return this.x + this.y
  }
}

The code here is invalid and doesn't work, because classes can only implement another class or an interface. Which makes sense since a class is a blueprint afterall and therefore it can't inherit one or another's shape structure.

However it does make sense to union Shape and Perimeter on just a regular object:


const rectangle: RectangleShape {
  x: 12,
  y: 33,
  area() => {
    return 2 * (rectangle.x + rectangle.y)
  },
  perimeter() => {
    return rectangle.x + rectangle.y
  }
}

In this example, the rectangle object must contain a method on it that matches the area or perimeter signature. If not, TypeScript will complain about the lack thereof.

2. Extending interfaces with types

Similarly to classes, you can't extend an interface with a type alias if you're using the union operator within your type definition. Just like a class, an interface is a static blueprint, and can't exist in one or another's shape.

type ShapeOrPerimeter = Shape | Perimeter

// Ts error: An interface may only extend a class or another interface
interface RectangleShape extends ShapeOrPerimeter, Point {}

3. Declaration merging with types

While merging declarations may work with interfaces, this is not the case for types. If you're unfamiliar with declaration merging, essentially it's where you define the interface multiple times, and the duplicates merge into one.

interface Box {
  height: number
  width: number
}

interface Box {
  scale: number
}

const box: Box = { height: 5, width: 6, scale: 10 }

This doesn't work with type aliases, as a type is a unique entity for both global, and module scope; This is also the only use case in which you should definitely be using interfaces.

Declaration merging via interfaces is very important for when we are writing 3rd party type definitions for libraries that are not authored with TypeScript. That way the user has the option to extend them if need be.

So, what should I be using for React?

In general, it doesn't really matter, just be consistent. Personally, I prefer interfaces over types most of the time until I need to create a type specifically. This occurs the most when you need to merge declarations coming from types or interfaces into a single type.

For example:


interface OwnProps = {...};
interface StoreProps = {...};

type Props = OwnProps & StoreProps;

Conclusion

In this article we covered the minor differences between types aliases and interfaces and came to the conclusion that there are times where you should be using one over the other.

Recap

  • Type aliases can act like interfaces, however, there are three important distinctions (union types, extending interfaces, declaration merging)
  • Use whichever you prefer, just be consistent.
  • Always use interfaces for defining public API's when authoring a library or 3rd party type definitions.