Skip to main content

Command Palette

Search for a command to run...

The Untapped Power of Interfaces in TypeScript - Create Modular Code with Interfaces

Updated
5 min read
The Untapped Power of Interfaces in TypeScript - Create Modular Code with Interfaces
M

Full Stack JS Engineer | Content Creator | I'm on a personal mission to provide high quality content on building web apps from code to production through my blog and my YouTube channel.

Most people believe that interfaces are simply "types for objects". What if I told you that is 100% not the purpose of interfaces? On YouTube I see a lot of hatred towards interfaces for the favor of type aliases. You will always see clickbait thumbnails of "Use types not interfaces". I have even seen a video where the creator claimed "he can't believe that the documentation suggests we should use interfaces". This blog is meant to challenge the status quo in the development community and shed some light on interfaces and why they are powerful in creating modular and reusable code.

In JavaScript, we have 2 main paradigms: functional programming, and object-oriented programming. Interfaces are specific and explicit to OOP and cannot be used in functional programming. So, if you want to see the power of interfaces you have to see them in action in OOP. Lacking the correct context when discussing interfaces creates cognitive biases by providing an incomplete picture of the topic at hand. The fact that criticizing interfaces by comparing them to type aliases is in itself a logical fallacy. Why? Because they are simply meant for different things.

If you want to see a video of me explaining the topics more in detail, please watch my video on YouTube:

1) Creating Blueprints for Classes

When creating classes, sometimes we need to define a structure of how our classes should look like and what properties and methods to be defined inside those classes. Let's see an example:

class HttpService {
  getUser() {
    // Get user logic
    console.log("Fetching user");
  }

  updateUser() {
    // Update user logic
    console.log("Updating user data");
  }
}

We might not have a single service that we need to implement in our program. We might have a LoggerService or GraphQLService or something similar. And we might want to define a blueprint for all those services so that if any new developer touches our codebase, they get a guidance from the typing system of TypeScript on how they should define the new services. We achieve that blueprint definition by using an interface as follows:

interface AccessDatabase {
  getUser: () => void;
  updateUser: () => void;
}

class HttpService implements AccessDatabase {
  async getData(url: string) {
    const response = await axios.get(url);
    const data = await response.data;
    console.log(data);
    return data;
  }

  async updateUser() {
    // Update user logic
    console.log("Updating user data");
  }
}

By "implementing" an interface, classes receive a blueprint on how they should be defined. If an engineer decided to name the function fetchUser instead of getData , TypeScript will throw an error letting us know that we're doing something wrong. This is extremely powerful during development.

2) Defining Contracts Between Classes - Dependency Injection/Composition/

Let's create a class calles User that makes use of the HttpService we defined earlier. To achieve that, we will create a contract between the User and the HttpService classes using interfaces.

import axios from "axios";
import { AccessDatabase } from "./services/HttpService";

export class User {
  // 2. Composition
  // 2. Delegating logic
  // 2. Contract between classes (dependency injection)
  constructor(private httpService: AccessDatabase) {}

  async getUserProfile(userId: number) {
    const url = `https://jsonplaceholder.typicode.com/users/${userId}`;
    return this.httpService.getData(url);
  }
}

In the class above, you see that the logic for fetching the user's data is not inside the User class. The reason is because the User class does not care about the implementation of HOW to fetch the user's data. It only cares about fetching that data and returning it. In other words, we delegated the logic of fetching data (user here and maybe products later) to another class.

Now whenever we want to instantiate the User class, we have to pass an object argument that satisfies the rules of the httpService interface. What is this object? It is an instance of the HttpService class. Thus, we have created a dependency between those two classes. We injected the dependency through an interface. This dependency injection is a form of a design pattern called Composition that allows us to create modular and reusable code.

// index.ts
const user = new User({ invalid: 'error' }); // invalid argument

const service = new HttpService();
const user = new User(service}); // no errors

Now we can easily swap the httpService injection inside the User class and use another interface. It's that easy.

Is this knowledge useful in the real world? If you ask YouTube front end developers, they will say what everyone says "interfaces and type aliases are ways to define types for objects, but types have xyz features that interfaces don't, so interfaces are useless."

As a full stack JavaScript and TypeScript engineer, I can give you a real life scenario where dependency injection is the core concept. I'm talking about the powerful NestJS framework. In NestJS we have services which are injectable classes that can be injected into controllers. Services are classes responsible for talking to a database, while controllers are classes responsible for using the services. I used the word using a class because the controllers delegate the logic of talking to the database to the service instead of implementing the logic directly inside the controller. This creates very powerful modular code.

There are other benefits for interfaces like testing, but it's beyond the scope of my discussion because I was simply aiming to shed light on the power of interfaces beyond what YouTube videos say.

Anyway did you see me mentioning anything about interfaces as types for objects? No, because that's simply not their purpose. The next time you see someone saying that "interfaces are simply types for objects", I would really appreciate it if you send them this blog or my YouTube video. Thank you for reading.

D

Interfaces are specific and explicit to OOP.

Are you serious?

2
M

Thanks for pointing that out. I updated the context to make my idea more clear:

In JavaScript, we have 2 main paradigms: functional programming, and object-oriented programming. Interfaces are specific and explicit to OOP and cannot be used in functional programming.

D

Mark Maksi because you updated your article, I am assuming you are not joking. Interfaces can, and are used in functional programming. Interfaces and classes are a nice way to group behaviours, and that is not exclusive to OOP

M

Daniel Rodríguez Rivero I updated the post because I don’t want my readers to think that interfaces are exclusive to TypeScript.

How can Interfaxes be used in functional programming other than defining types for objects? Can you show me an example in a code snippet?

D

Mark Maksi interfaces allow some type definitions that are not possible in plain types. Not sure what is in your mind, but ojects and classes are not exclusive to OOP. In FP classes can be used to model/group behaviour, without attaching any data to it. Option, Either are defined using interfaces that any class can implement. Here you have an example: https://github.com/gcanti/fp-ts/blob/master/src/Option.ts

More from this blog

Mark Maksi

20 posts