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

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

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.