bydn.mo@gmail.com

Types ftw!!! - 17/06/2024

types >>>>>

The many ways that type diffs interface heavily; a short blog 🤓

So far we’ve heard a lot about how both types and interfaces have things going for them. But I am here to say that, actually, everything interfaces have going for them, types have and they do it better.

Generic example of something you can’t do purely with interfaces:

type Success = {
    status: "success";
    data: any;
}

type Error = {
    status: "error";
    message: string;
}

type Result = Success | Error;

While you can have a type that is a union of 2 interfaces, this is undesirable as it would introduce both paradigms into a project and bring overall greater uncertainty and inconsistency; especially if you are onboarding newer developers. It doesn’t take a genius to tell you that standards are important.

The following is an extension of the previous example with even more type magic ✨:

type Status = "200" | "201" | "400" | "404" | "500";

type BaseResponse<T extends Status> = {
    status: T;
    // Any other field a generic response would have
}

type SuccessfulResponse = BaseResponse<"200" | "201"> & {
    data: any;
}

type ErrorResponse = BaseResponse<"400" | "404" | "500"> & {
    message: string;
}

type Result = SuccessResponse | ErrorResponse;

The result of this implementation provides you powerful types that give meaningful constraints to its fields. When defining a SuccessfulResponse for example, the status field will be constrained to only using successful status codes, or risk facing the wrath of the red squiggly line and the infamous memory dump-like Typescript warning message (iykyk).

You may have also noticed the use of the & symbol. Let’s take a look at what it does! Let us say we have the following type definition:

type User = {
    id: number;
    name: string;
}

I would like to note that the fact it is a type and not an interface has no bearing on this experiment and both function exactly the same in this scenario.

Now, if we want to extend this type to include the role field, this is is how we would do it using an interface 🤢:

interface UserWithRole extends User {
    role: string
}

Another important note is the fact that interfaces can extend both other interfaces and types. This is awesome because if you’re writing some sort of component library or package for example, your decision to adopt a standard of only using types does not hinder the extensibility of your app too greatly with (I believe) the only exception being the fact that interfaces cannot extend union types. Being that there is no alternative to union types, I would mark this as a strike against interfaces.

This is how you would extend a type or an interface using what is called an intersection (if you are familiar with set theory, the terminology used in types makes a lot more sense):

type UserWithRole = User & {
    role: string
}

Another thing to consider is that interfaces can only extend one other interface while intersections can be used to provide the same effect as extending multiple interfaces or types:

type Burger = Meat & Lettuce & Tomato & {
    price: number;
}

In Conclusion

When it comes down to it, types simply provide a better developer experience with their flexibility and simple, yet powerful syntax.

interfaces are a product of an olden time, and its very own name dates it. Interfaces are in fact, not the same interfaces that many developers are familiar with from other languages. New developers learn types nowadays and using interfaces is just plain confusing as no other paradigm refers to interfaces as a container for data.

This all to say, type >>>>>>>>>>> interface