Classes and interfaces are powerful structures that facilitate not just object-oriented programming but also type-checking in TypeScript. A class is a blueprint from which we can create objects that share the same configuration - properties and methods. An interface is a group of related properties and methods that describe an object, but neither provides implementation nor initialisation for them.

Once you’re finished, check out my other article on TypeScript Interfaces vs Types!

Since both of these structures define what an object looks like, both can be used in TypeScript to type our variables. The decision to use a class or an interface truly depends on our use case: type-checking only, implementation details (typically via creating a new instance), or even both! We can use classes for type-checking and the underlying implementation - whereas we cannot with an interface. Understanding what we can get from each structure will easily let us make the best decision that will enhance our code and improve our developer experience.

Using TypeScript class

ES6 introduced class officially to the JavaScript ecosystem. TypeScript boosts JavaScript classes with extra power such as type-checking and static properties. This also means that whenever we transpile our code to whatever target JavaScript of our choice, the transpiler will keep all of our class code present in the transpiled file. Hence, classes are present throughout all the phases of our code.

We use classes as object factories. A class defines a blueprint of what an object should look like and act like and then implements that blueprint by initialising class properties and defining methods. Therefore, when we create an instance of the class, we get an object that has actionable functions and defined properties. Let’s look at an example of defining a class named PizzaMaker :

class PizzaMaker { static create ( event : { name : string ; toppings : string [] }) { return { name : event . name , toppings : event . toppings }; } }

PizzaMaker is a simple class. It has a static method called create . What makes this method special is that we can use it without creating an instance of the class. We just invoke the method on the class directly - much like we would with something like Array.from :

const pizza = PizzaMaker . create ({ name : ' Inferno ' , toppings : [ ' cheese ' , ' peppers ' ], }); console . log ( pizza ); // Output: { name: 'Inferno', toppings: [ 'cheese', 'peppers' ] }

Then, PizzaMaker.create() returns a new object - not a class - with a name and toppings properties defined from the object passed to it as argument.

⚡️ FREE eBook: 🔥 ForEach, Map, Filter, Reduce, Some, Every, Find “This book is straight to the point, syntax exploration, comprehensive guide, real-world examples, tips and tricks - it covers all you need Todd Motto, author of Exploring JavaScript Array Methods

If PizzaMaker did not define create as a static method, then to use the method we would need to create an instance of PizzaMaker :

class PizzaMaker { create ( event : { name : string ; toppings : string [] }) { return { name : event . name , toppings : event . toppings }; } } const pizzaMaker = new PizzaMaker (); const pizza = pizzaMaker . create ({ name : ' Inferno ' , toppings : [ ' cheese ' , ' peppers ' ], }); console . log ( pizza ); // Output: { name: 'Inferno', toppings: [ 'cheese', 'peppers' ] }

We get the same output we had with create as a static method. Being able to use TypeScript classes with and without an existing instance of a class makes them extremely versatile and flexible. Adding static properties and methods to a class makes them act like a singleton while defining non-static properties and methods make them act like a factory.

Now, unique to TypeScript is the ability to use classes for type-checking. Let’s declare a class that defines what a Pizza looks like:

class Pizza { constructor ( public name : string , public toppings : string []) {} }

In the Pizza class definition, we are using a handy TypeScript shorthand to define class properties from the arguments of the constructor - it saves a lot of typing! Pizza can create objects that have a name and a toppings property:

const pizza = new Pizza ( ' Inferno ' , [ ' cheese ' , ' peppers ' ]); console . log ( pizza ); // Output: Pizza { name: 'Inferno', toppings: [ 'cheese', 'peppers' ] }

Aside from the Pizza name before the pizza object that shows that the object is in fact an instance of the Pizza class, the output of new Pizza(...) and PizzaMaker.create(...) is the same. Both approaches yield an object with the same structure. Therefore, we can use the Pizza class to type-check the event argument of PizzaMaker.create(...) :

class Pizza { constructor ( public name : string , public toppings : string []) {} } class PizzaMaker { static create ( event : Pizza ) { return { name : event . name , toppings : event . toppings }; } }

We’ve made PizzaMaker much more declarative, and hence, much more readable. Not only that, but if we need to enforce the same object structure defined in Pizza in other places, we now have a portable construct to do so! Append export to the definition of Pizza and you get access to it from anywhere in your application.

Using Pizza as a class is great if we want to define and create a Pizza , but what if we only want to define the structure of a Pizza but we’d never need to instantiate it? That’s when interface comes handy!

Using TypeScript interface

Unlike classes, an interface is a virtual structure that only exists within the context of TypeScript. The TypeScript compiler uses interfaces solely for type-checking purposes. Once your code is transpiled to its target language, it will be stripped from its interfaces - JavaScript isn’t typed, there’s no use for them there.

And, while a class may define a factory or a singleton by providing initialisation to its properties and implementation to its methods, an interface is simply a structural contract that defines what the properties of an object should have as a name and as a type. How you implement or initialise the properties declared within the interface is not relevant to it. Let’s see an example by transforming our Pizza class into a Pizza interface:

interface Pizza { name : string ; toppings : string []; } class PizzaMaker { static create ( event : Pizza ) { return { name : event . name , toppings : event . toppings }; } }

Since Pizza as a class or as interface is being used by the PizzaMaker class purely for type-checking, refactoring Pizza as an interface did not affect the body of the PizzaMaker class at all. Observe how the Pizza interface just lists the name and toppings properties and gives them a type. What also changed is that we cannot create an instance of Pizza anymore. Let’s further explain this core difference between interface and class by considering Pizza as a class again.

Using TypeScript class vs using Typescript interface

As it is, our current code provides type-checking for Pizza but can’t create a pizza:

interface Pizza { name : string ; toppings : string []; } class PizzaMaker { static create ( event : Pizza ) { return { name : event . name , toppings : event . toppings }; } }

This is unfortunate because we are missing a golden opportunity to further improve the declarative nature and readability of our code. Notice how PizzaMaker.create() returns an object that surely looks a lot like a Pizza would! It has a name that is a string and it has toppings that is a string array - we infer the property types from the type of event which is Pizza . Wouldn’t it be awesome if we could return an instance of Pizza from within PizzaMaker.create() ?

As mentioned many times earlier, we can’t instantiate the Pizza interface, doing so will trigger an error. However, we can refactor again Pizza to be a class and then return an instance of Pizza :

class Pizza { constructor ( public name : string , public toppings : string []) {}; } class PizzaMaker { static create ( event : Pizza ) { return new Pizza ( event . name , event . toppings ); } } const pizza = PizzaMaker . create ({ name : ' Inferno ' , toppings : [ ' cheese ' , ' peppers ' ] };

We enforce the structure that the event argument of PizzaMaker.create() takes whilst still being able to create the object that the type Pizza as a class defines! We get the best of both worlds here - the blueprint and the contract. It’s up to you which one you need for your use cases.

Learn about TypeScript Interfaces vs Types next!

Conclusion

We’ve learned a lot, without really diving into a huge amount of code. The tl:dr; is if you need/wish to create an instance of perhaps a custom object, whilst getting the benefits of type-checking things such as arguments, return types or generics - a class makes sense. If you’re not creating instances - we have interfaces at our disposal, and their benefit comes from not generating any source code, yet allowing us to somewhat “virtually” type-check our code.

If you are serious about your TypeScript skills, your next step is to take a look at my TypeScript courses, they will teach you the full language basics in detail as well as many advanced use cases you’ll need in daily TypeScript development!

Since both an interface and a class define the structure of an object and can be used interchangeably in some cases, it’s worth noting that if we need to share structural definition amongst various classes, we can define that structure in an interface and then have each class implement that interface! Each class then will have to declare or implement each property of the interface. That’s the power of TypeScript, and it’s also super flexible. We have comprehensive object-oriented design paired with versatile type-checking.