Exploring Typescript

Interfaces, Types & Classes...Oh My!

Subscribe to my newsletter and never miss my upcoming articles

Table of Contents

Why are types important in typescript?

Typescript is a superset of Javascript, which means that all Javascript is syntactically valid Typescript. However, Typescript's power comes from its strict typing of objects (no surprise there). Type checking often focuses on the shape of an object, ensuring that every object of a particular type adheres to a set of guidelines. For example, once a variable (or object) has been set to a particular type, it cannot be changed later...

let alpha = 'a';
alpha = 1 // this will not work since alpha was 
          // originally typed as a string

Interfaces vs. Types

Interfaces and types are the two most prevalent structures in which we see the power of Typescript and are, for the most part, completely exchangeable. One of the key differences between the two is that interfaces can be 'mutated' while types cannot. What do I mean by this? Let's look at an example.

While with interfaces we are allowed to add new fields to the interface, hence mutating the object type. This does not create two declarations, nor does it override the original declaration, but rather merges them...

CleanShot 2021-03-15 at 23.58.40@2x.png

...with types, not so much. When trying to merge two declarations, we will see this 'Duplicate Identifier' error.

CleanShot 2021-03-15 at 23.59.20@2x.png

Mapped Types

This was one of the coolest properties of types that I have learned, the ability to use computed properties...so what does this mean and why is it useful?

type Fields = "breed" | "name" | "color";
type Dog = {
     [key in Fields]: string
}
const myDog: Dog = {
     breed: "Poodle",
     name: "Fluffy", 
     color: "black"
}

This allows us to be incredibly explicit about the necessary fields in a type, provides a simple way to increase code readability, and since all of the required fields are laid out in a separate type, those requirements can be used in other aspects of the code without having to constantly remember the requirements when defining new types and variables. However, one thing to note here is that these mapped types work with dictionary types, where fields can be a single type, but not so well with complex types, where fields can be one of many types.

This is one of the places where types outshine interfaces; this ability to use mapped types doesn't work with interfaces.

CleanShot 2021-03-25 at 22.27.49@2x.png

Intersections & Unions

An intersection of two sets is the set of elements found in both sets...well, in set theory at least. In TYPE theory, it's literally the opposite, and I definitely got this wrong initially. So how does this actually work in typescript? I find it more useful to think about intersection and unions in terms of their boolean counterparts, AND and OR (which coincidentally are also the operators we use to define intersections and unions in typescript syntax, & and I). We can use intersections to create new types from other types...

type Breed = {
     breed: string,
     sizeInLbs: number
};

type Age = {
     age: number
};

type Dog = Breed & Age;

...and from interfaces...

interface Breed {
     breed: string,
     sizeInLbs: number
};

interface Age {
     age: number
};

type Dog = Breed & Age;

We can think of an intersection of types like we would the boolean operator and, so by defining the type Dog as the intersection of Breed and Age, we get a type that is both Breed AND Age.

CleanShot 2021-04-08 at 12.17.32.png

Similarly, we can define types by computing the union of two other types or interfaces. A union of two types can be thought of like the boolean operator or, so in our above example, the union of Breed and Age would render a type that is either Breed OR Age.

CleanShot 2021-04-19 at 09.37.11@2x.png

In this above example, the property hasHair is only defined on Cat, so when we define pet with type='dog', Typescript knows that hasHair is not a valid property (since the union type of pet is known to be Dog). In other words, typescript has discriminated the type of pet as type Dog based on the properties we have used to define it.

If we had defined pet using the property type='cat' instead, then Typescript would have complained about the property earShape not being a valid property of type Cat.

Object-Oriented Typescript

So far, it seems like types outperform interfaces; they seem to allow more flexibility in defining and re-defining types. However, there is one area of programming where interfaces provide more value; object-oriented programming (OOP). OOP is a paradigm of programming that relies on the use of classes and objects to define data structures...interfaces in typescript provide a bridge between typing and OOP. This is done using the concepts of 'extends' and 'implements', two ways of defining classes and using interfaces to enforce the data structure contracts laid out by those classes.

class Dog {
     sayHello = () => {
          console.log("Hello! I'm a dog")
     };
};
interface Pet extends Dog {
     name: string
}

We have just defined an interface called "Pet" which not only has a name, but also has access to the methods of the class "Dog", thus defining a hierarchy of sorts (all dogs are pets, but not all pets are dogs). We can also define a class by implementing an interface...

class FavoritePet implements Pet {
     name: string,
     constructor(name: string) {
          this.name = name;
     }
     sayHello = () => {
          console.log("Hello! I'm a dog")
     };
}

At this point, you may be wondering "what's the difference between classes and interfaces then?" Interfaces are essentially virtual structural definitions while a class provides the ability for initialization of instances, thus making the virtual definition more concrete. Another way to look at this is that classes can include actual implementations while interfaces are just 'contracts' to be followed.

Bottom Line

Interfaces are basically a way of describing a data shape or defining a contract. Meanwhile, a type is usually used for composition of other types, like a union or intersection. That being said, they are often used interchangeably, and the most important thing to remember is to be consistent in how they are used within a codebase. As a software engineer, it is also important to always think about use-cases before writing code and using built-in features of a language, and this is true when using interfaces and types as well!

Resources

-- Maya S.

No Comments Yet