DEV Community

Cover image for Of Type and Interface: TypeScript Tales Ch. 01
A. Rahman Bishal
A. Rahman Bishal

Posted on

Of Type and Interface: TypeScript Tales Ch. 01

As you are reading this, I’m going to assume that you have a basic idea of what TypeScript is (which is basically JavaScript with syntax for types, according to the official docs). The purpose of this piece of writing is to discuss and look into the contrast between two commonly used constructs widely used in TypeScript: Type, and Interface.

In short, we use type as type aliases, which is, a name for any type. With the help of this, we can use the same type more than once and refer to it by a single name. On the other hand, interface, or interface declaration is basically another way to name an object type, actually a contract for a structure.

Now, they may not seem very similar by the definition. But here we are here, talking about what are the differences, and maybe trying to choose only one between type and interface, there should be some similarities that might make you confused, right?

In a bunch of cases, we can use type and interface interchangeably, and choose them freely. For object shapes and function signatures, both can work the same way. Let’s see an example. We will define objects and functions using both type and interface.

// defining object

type ObjectType = {
  name: string;
  author: number;
  fun(): void;
}

let thrillerBook: ObjectType;

interface ObjectInterface {
  name: string;
  author: number;
  fun(): void;
}

let fantasyBook: ObjectInterface;

// defining function

type FunctionType = (author: string) => string;

const getAcademicAuthor: FunctionType = (author) => {
  return `The academic author is ${author}`;
}

interface FunctionInterface {
  (author: string): string;
}

const getFictionAuthor: FunctionInterface = (author) => {
  return `The fiction author is ${author}`;
}
Enter fullscreen mode Exit fullscreen mode

But yes, there are some obvious differences. Both of the keywords has some superpowers of their own, that the other one does not have. Let’s dive into those.

Superpowers of type

You can use type to represent anything from primitives, unions, tuples, and various complex type. But interface is mostly limited to defining shapes e. g. objects (as well as callable & constructable signatures).

type Title = string;                // primitive
type Published = string | number;   // union
type Binding = [string, string];    // tuples
type Spine = "flat" | "round";      // template literal
type ReadonlyBook = {
  readonly [K in keyof Book]: Book[K];
}                                   // mapped types
Enter fullscreen mode Exit fullscreen mode

There are cases, when we must fall back to using type alias. We can roughly list them down like this:

  • Union types
  • Intersection types
  • Mapped types
  • Conditional types
  • Template literal types

These are some mere examples that interface does not support. It can be used for far more complex typing.

Superpowers of interface

While using interface for defining shapes or structure, there are several perks over using type. One of the major distinctions between type and interface is that type cannot be re-opened and redeclared, whereas interface can be. We call this declaration merging.

interface Book {
  title: string;
  author: string;
}

interface Book {
  published: number | string;
  reprint(): void;
}

// we can do this, no error.
Enter fullscreen mode Exit fullscreen mode

Now, if we declare an object of type Book, it will have all the properties from both definitions, i. e. title, author, published, reprint(). But such is not the case for type.

type Movie = {
  title: string;
  released: number;
}

type Movie = {
  director: string;
}

// we cannot do this, TS will throw error.
Enter fullscreen mode Exit fullscreen mode

To achieve the same with type, we must define a new type, and intersect the old one with it. Something like this:

type MovieWithDirector = Movie & {
  director: string;
}
Enter fullscreen mode Exit fullscreen mode

But this method has a little overhead comparing to the one we saw earlier with declaration merging.

Even though declaration merging is one of the biggest superpowers of interface, it comes with a warning label—overusing merging can make the codebase harder to reason about.

Extensions and Implementations

An interface can extend both interface, and a type. Remember the Book interface and the Movie type we defined earlier in the example? Let’s extend those:

// interface extends another interface
interface PaperbackBook extends Book {
  binding: "paperback";
}

// interface extends another type alias
interface AnimatedMovie extends Movie {
  medium: string;
}
Enter fullscreen mode Exit fullscreen mode

But remember, for an interface to be able to extend a type alias, it must be of an object type. For example, the below extension is not possible.

type Magic = number | string | [string, string] | number[];

interface Technology extends Magic {
    ...
}

// this will throw an error
Enter fullscreen mode Exit fullscreen mode

The bottom line is, interfaces can extend other interfaces and sometimes type aliases if they define shapes. But the opposite is not possible in the same way. For that, we have to use intersection, as we did before to mock declaration merging with type aliases.

This scenario remains the same if a class tries to implement either of these. Let’s continue with the previously declared Book interface and Movie type.

// a class implements an interface
class FantasyBook implements Book {
  title = "The Hobbit";
  author = "John Ronald Reuel Tolkien";
  published = 1937;
  reprint() {
    console.log("reprint this again!");
  };
}

// a class implements a type alias
class AnimatedMovie implements Movie {
  title = "Tarzan";
  released = 1999;
  isFavouriteMovie = true; 
}
Enter fullscreen mode Exit fullscreen mode

But, same as before, the type alias must define a shape i.e. an object. Otherwise, it cannot be implemented.

type NoShape = number | string | [string, string] | number[];

class Shape implements NoShape {
    ...
}

// this will throw an error
Enter fullscreen mode Exit fullscreen mode

So, for its very nature, interface can be extended or implemented however we like, without any worries, as they define a shape. But as type alias is more of a broader toolbox for type definition, there are some limitations and cases where we need to be careful.

Now, the end is nigh, and I shall conclude this with a million dollar question:

Type or Interface, which one to use?

As both type and interface share some common use cases, the question is: which one to use?

Well, actually, there is no obvious choice. At the end, it really depends on you. But before deciding that, here are some facts:

  • TypeScript provides better and more focused error messages while working with interface.
  • Interface was supposed to be slightly more performant than type. But this difference is practically negligible in most cases (i. e. if the defined type is not very complex or deeply nested), specially in the later versions. After version 4.0, there is no measurable performance difference in real-world scenarios—stated publicly by the TypeScript Team.
  • Interface has closer mapping with objects by definition.

In case of common use cases, like defining an object, or writing a contract for a class, using interface seems more intuitive from object oriented perspective. But we have no way to avoid type in some cases also i. e. while working with union types or so.

The official TypeScript docs suggests to "Use interfaces until you need to use features from type aliases". This pretty much infers to “Use interfaces to define the shape of objects. Use type aliases for more advanced type composition.” For your convenience, I can provide a decision tree—

Use interface when:

  • You want declaration merging.
  • You want cleaner IntelliSense names.
  • You want the error messages to make more sense.

Use type when:

  • You need unions, intersections, mapped types, template literals.
  • You work with utility types (Partial, Pick, etc.).

But if you are still confused, just go with whichever you are comfortable with. But, it is recommended to be consistent on the choice. Choose the approach that better suits your style and keeps the code more readable and maintainable.

Keep in mind that, these are compile-time only constructs. After being compiled into JavaScript, no trace of type or interface remains. So choosing one or another does not affect runtime perfomance.

References

TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html

Playground Examples 1: https://tinyurl.com/4cwazrus

Playground Examples 2: https://tinyurl.com/4hu8ebsy

Top comments (0)