Interfaces and classes are the fundamental parts of object-oriented programming (OOP). TypeScript is an object-oriented JavaScript language that, from ES6 and later, supports OOP features like interface, class, and encapsulation.
But when should we use interfaces, classes, or both at the same time? If you are a new or confused using interfaces and classes, this piece is for you.
In this article, I’ll show you what interfaces and classes are and when to use one or both of them in TypeScript.
Before we get started, we need to know what a TypeScript class is. In object-oriented programming, a class is a blueprint or template by which we can create objects with specific properties and methods.
Typescript provides additional syntax for type checking and converts code to clean JavaScript that runs on any platform and browser. Classes are involved in all stages of code. After converting the TypeScript code to a JavaScript file, you can find them in the final files.
The class defines the template of the object, or what it is and what it does. Let’s create a simple class with properties and methods so we can see how it will behave.
First, I’m going to create a Developer
class through the following lines of code:
class Developer { name?: string; // string or undefined position?: string; // string or undefined }
We describe the class with properties name
and position
. They contain types like string
and undefined
.
Next, let’s create an object via the Developer
class using the new
keyword:
const developer = new Developer(); developer.name // it outputs undefined developer.position // it outputs undefined
When we call developer.name
, it returns undefined
because we didn’t assign initial values. In order to create an object with values in TypeScript, we can use the constructor
method. The constructor method is used to initialize and create objects.
Now we update our Developer
class with the following code:
class Developer { name: string; // only string position: string; // only string constructor(name: string, position: string) { this.name = name; this.position = position; } }
In the code above, we added the constructor
method to initialize the properties with values.
Now we are able to set the name
as Gapur
and position
as Frontend Developer
using the following code:
const developer = new Developer("Gapur", "Frontend Developer"); developer.name // it outputs Gapur developer.position // it outputs Frontend Developer
Last, as I mentioned earlier, the class has methods that how the object should act. In this case, any developer develops applications, therefore, the Developer
class has the method develop
.
Thus, a developer
object can perform a development action:
class Developer { name: string; position: string; constructor(name: string, position: string) { this.name = name; this.position = position; } develop(): void { console.log('develop an app'); } }
If we run the develop
method, it will execute the following console.log
statement:
developer.develop() // it outputs develop an app
An interface is a structure that acts like a contract in your application, or the syntax for classes to follow. The interface is also known as duck printing, or subtyping.
The interface includes an only
method and field declarations without implementation. We can’t use it to create anything. A class that implements an interface must have all fields and methods. Therefore, we use them for type checking.
When TypeScript converts all code to JavaScript, the interface will disappear from the JavaScript file. Therefore, it is a helpful tool during the development phase.
We should use an interface for the following:
Now, let’s declare the interface through the following lines of code:
interface InterfaceName { // variables; // methods; }
We can only contain declarations of variables and methods in the body of the interface. Let’s create an IDeveloper
interface for the previous Developer
class:
interface IDeveloper { name: string position: string develop: () => void } class Developer implements IDeveloper { name: string; position: string; constructor(name: string, position: string) { this.name = name; this.position = position; } develop(): void { console.log('develop an app'); } }
In the above code, our IDeveloper
interface contains the variables name
and position
. It also includes the develop
method. So the Developer
class implements the IDeveloper
interface. Thus, it must define two variables and a method.
If the Developer
class doesn’t implement any variables, TypeScript will show an error:
class Developer implements IDeveloper { // error Class 'Developer' incorrectly implements interface 'IDeveloper'. name: string; constructor(name: string, position: string) { this.name = name; this.position = position; } develop(): void { console.log('develop an app'); } }
So when should we use classes and when should we use interfaces?
Before we start, I want to share with you the powerful TypeScript static
property that allow us to use fields and methods of classes without creating instance of class.
I am going to make a class with a static method using the previous Developer
class:
class Developer { static develop(app: { name: string, type: string }) { return { name: app.name, type: app.type }; } }
Now, we can just call the Developer.develop()
method without instantiating the class:
Developer.develop({ name: 'whatsapp', type: 'mobile' }) // outputs: { "name": "whatsapp", "type": "mobile" }
Great!
Also, we can use classes for type checking in TypeScript. Let’s create an App
class using the following code:
class App { name: string; type: string; constructor(name: string, type: string) { this.name = name; this.type = type; } }
Let’s modify our Developer
class:
class Developer { static develop(app: App) { return { name: app.name, type: app.type }; // output the same } }
Now I will make an App
instance and invoke Developer.develop()
with an argument object:
const app = new App('whatsapp', 'mobile'); Developer.develop(app); // outputs the same: { "name": "whatsapp", "type": "mobile" }
Developer.develop(app)
and Developer.develop({ name: 'whatsapp', type: 'mobile' })
output the same content. This is awesome, but the second approach is more readable and flexible.
Plus, we can check the type of arguments. Unfortunately, to do so, we need to create an object. So how can we improve it? This is where the interface comes in!
First, I am going to change the App
class to an interface with the following code:
interface App { name: string type: string } class Developer { static develop(app: App) { return { name: app.name, type: app.type }; // output the same } }
In the code above, we didn’t change the body of the Developer
class and didn’t create an instance of App
, but the result was the same. In this case, we saved a lot of time and code typing.
When should we use classes and interfaces? If you want to create and pass a type-checked class object, you should use TypeScript classes. If you need to work without creating an object, an interface is best for you.
Eventually, we opened two useful approaches: blueprints and contracts. You can use both of them together or just one. It is up to you.
Thanks for reading — I hope you found this piece useful. Happy coding!
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.