Typescript: Classes vs. Interfaces

When most people start using Typescript, they run into the inevitable question: Class or Interface?: How do I choose which one to use? How does it affect my design? How does it affect my end code that runs? Is there actually a difference? I realized that the answer was not obvious and after nearly 10,000 views, the question on Stack Overflow, Difference between interfaces and classes in Typescript, had not been answered. So I sent out to answer the question. I will outline the differences in syntax, and when and when not use to each and what happens to them when they get transpiled to Javascript. I posted the shortened, non-example version here.

First, there is the obvious difference: syntax. This is a simple, but necessary to understand difference: Interface properties can end in commas or semi-colons, however class properties can only end in semi-colons. Now the interesting stuff. The sections about when to use and not to use may be subjective – these are the guidelines I give people on my team, but it is possible that other teams will have other guidelines for valid reasons. Feel free to comment if your team does it differently, I would love to learn why.

Interfaces: Allow for defining a type that will be used during design and compile time for strong typing. They can be “implemented” or “extended” but cannot be instantiated (you can’t new them). They get removed when transpiling down to JS so they take up no space, but they also cannot be type checked during runtime, so you can’t check if a variable implements a specific type at runtime (foo instanceof bar), except by checking the properties it has: Interface type check with Typescript.

Let’s look at the impact to your JS transpiled code for interfaces:

interface InterfaceExample {
    status: number; // Some HTTP status code, such as 200
}

const interfaceExample : InterfaceExample = { status: 200 };

The JS code simply becomes:

var interfaceExample = { status: 200 };

When to use interfaces: Use them when you need to create a contract of the properties and functions for an object that will be used in more than one place in your code, especially more than one file or functions. Also, use when you want other objects to start with this base set of properties, such as having multiple classes that all should start with the same set of.

When not to use interfaces: When you want to have default values, implementations, constructors, or functions (not just signatures).

Classes: Also allow for defining a type that will be used during design and compile time for strong typing, and, additional, can be used during runtime. This also means that the code is not compiled out, so it will take up more space. This is one key difference mentioned by @Sakuto, but has more implications than just space. It means that classes can be typed checked, retaining and understanding of “who they are” even in the transpiled JS code. Further differences include: classes can be instantiated using new and can be extended, but not implemented. Classes can have constructors and actual function code along with default values for variables.

Now let’s look at the impact to your JS transpiled code for classes too:

class ClassExample {
    status: number; // Some HTTP status code, such as 200
}

const classExample1: ClassExample = { status: 200 };

Which is the same size in TS, in JS ES5 code it becomes:

var ClassExample = /** @class */ (function () {
    function ClassExample() {
    }
    return ClassExample;
}());
var classExample1 = { status: 200 };

The size difference is quite substantial, and since I just used it as a type, it added no value. This should always be considered.

When to use classes: When you want to create objects that have actual function code in them, have a constructor for initialization, and/or you want to create instances of them with new. Also, for simple data classes that don’t contain any functions, you can use classes for setting up default values. Another time you would want to use them is when you are doing type checking, though there are workarounds for interfaces if needed (see the interface section OS link).

When not to use classes: When you have a simple data interface, do not need to instantiate (new) it, when you want to have it implemented by other objects, when you want to simply put an interface on an existing object (think type definition files) or when the space it would take up is prohibitive or unwarranted. As a side note, if you look in .d.ts files you will notice that they only use interfaces and types, and thus this is completely removed when transpiled to TS.

Here are some of differences

You could create this interface to store data about your car:

interface CarData {
    odometer: number;
    make?: string;
    model?: string;
    lastOilChange: number;
}

But what you are unable to do is set any default values. Every time you create an instance you are going to have to set the odometer and lastOilChange to 0 (assuming new cars) and then if you know the make and model you set them. Now, since they are marked as optional, when you go to use them you will always have to check if they are defined. Now if you were to write it as a class:  

class CarData {
    odometer = 0;
    make = "unknown";
    model = "unknown";
    lastOilChange = 0;
}

You can set default values, which will assure you always have a make and model set, albeit they are unknown to start with. Both of these are perfectly acceptable so the you would have to decide for your specific use case which is better. Perhaps the cars aren’t always new and you always should know the make and model, then having the first interface without making make and model optional may better suite your business needs.   Another example is if you had an interface with functions in it, like this:

interface Car {
    carData: CarData;
    drive: () => boolean;
    stop: () => boolean;
}
 
class FordTaurus implements Car {
    carData = { make: "Ford", model: "Taurus", odometer: 0, lastOilChange: 0 };
    driveTrain = {tryToDrive() {return true}, tryToStop() {return true}}
 
    drive() { return this.driveTrain.tryToDrive() }
    stop() { return this.driveTrain.tryToStop() }
}

Which would require every car to implement functions for drive() and stop() and any other internal components, which could be great unless all cars drive and stop the same way, in which case you could switch to a class that you extend:  

class Car2 {
    carData: CarData;
    driveTrain = {tryToDrive() {return true}, tryToStop() {return true}}
 
    drive() { return this.driveTrain.tryToDrive() }
    stop() { return this.driveTrain.tryToStop() }
}
 
class FordTaurus extends Car2 {
    carData = { make: "Ford", model: "Taurus", odometer: 0, lastOilChange: 0 };
}

This really comes down to a conversation about inheritance vs. composition, which is beyond the scope of this answer, I just want to show examples of one vs. the other. And just as a final note, there are two other options than just classes and interfaces, the first is something called a “type”, which is pretty similar to an interface, but check this SO post, specifically the 2019 Update answer: Typescript: Interfaces vs Types. The last option is to go functional programming style (not OOP) with TS. I don’t have much experience in this area, but I know that certain libraries, such as Redux, are functional, not OO, so you should know that it exists and it is something different. With functional programming if we wanted to do the above example we could just do:  

const FordTaurus2 = {
    carData: { make: "Ford", model: "Taurus", odometer: 0, lastOilChange: 0 },
    driveTrain: { tryToDrive() { return true }, tryToStop() { return true } },
 
    drive() { return this.driveTrain.tryToDrive() },
    stop() { return this.driveTrain.tryToStop() },
}

Feel free to pipe in if you have a better Functional Programming example, it is really out of the scope here and should have its own article, but I wanted to briefly touch on it.

For further reading you can check out https://jameshenry.blog/typescript-classes-vs-interfaces/ as well.  The meat I originally published on Stack Overflow: Difference between interfaces and classes in Typescript.

Appendix:

To compare and contrast interfaces vs. classes in their compiled code here where you can see the code in typescript playground that will show this example and how it looks in JS. Notice how it retains “ClassExample” as an identity for the object but then “classExample1” it doesn’t actually have a reference to the ClassExample object this makes instanceof not work. You must create an instance of the object (i.e. use ‘new’) in order to be able to use the instanceof, otherwise, it is just an expensive interface.