Last article, we talked about TypeScript classes, how to use them effectively, and all of the fundamental concepts associated with them.
In this article, we’ll create interfaces, learn how to use them, explore the differences between normal types and interfaces, and learn about declaration and merging.
WHAT IS AN INTERFACE?
An Interface is a structure that acts as a contract/doc in our application. It defines the syntax for classes/objects to follow, which means a class/object which implements an interface is bound to implement all its members (except for optional properties/methods).
The interface contains only the declaration of the methods and fields, but not the implementation. It is a shape of an object.
SYNTAX & DECLARATION:
CREATE AN INTERFACE:
Interfaces in TypeScript are created by using the interface
keyword followed by the name of the interface, and then a {}
block with the body of the interface. For example, here is a Course
interface:
interface Course {
name: string;
price: number;
isFree: boolean;
onPurchase: (userId: string) => void;
}
Similar to creating a normal type
using the type declaration, you specify the fields/properties of the interface
, and their types, in the {}
:
The above Course
interface represents an object that has four properties which are name
of type string
, price
of type number
, isFree
of type boolean
, and onPurchase
which is a function that accepts a single parameter userId
of type string
and returns void
.
USE THE INTERFACE AS A VALID TYPE:
Now the Course
interface is a valid TypeScript type. It can be used like any other type. An example of creating an object literal that matches the Course
interface is as follows:
let course: Course = {
name: "Angular",
price: 100,
isFree: true,
onPurchase: (userId: string) => {
console.log(`User with id ${userId} has purchased ${course.name}`);
},
};
Variables using the Course
interface as their type must have the same members as those specified in the Course
interface declaration.
If you miss any one of the members you will get a compilation error as follows:
OPTIONAL PROPERTIES:
If you want to resolve the above error, you must include the property or make the particular property optional
by adding ?
ahead of :
as follows:
interface Course {
name: string;
price: number;
isFree: boolean;
onPurchase?: (userId: string) => void; // this is an optional property
}
// No compilation error
let course: Course = {
name: "Angular",
price: 100,
isFree: true,
};
However, when you make any property optional, TypeScript automatically infers that property to be of type <your given type> | undefined
.
If you hover over the onPurchase
property, you'll see that TypeScript has inferred the type as ((userId: string) => void) | undefined
.
To avoid compile-time errors when working with such properties, type guards must be used. Type guards are nothing more than extra type-checking.
We will discuss Type guards in depth in future blogs.
READONLY PROPERTIES:
You can add read-only properties to an interface using the readonly
keyword. This is similar to the last time we discussed how to create read-only properties inside a class with the readonly
modifier.
Whenever we want to declare a property as read-only, we put the readonly
keyword before the name of the property inside an interface
, as shown below:
interface Course {
name: string;
readonly price: number; // readonly property
isFree: boolean;
onPurchase?: (userId: string) => void; // this is an optional property
}
// No compilation error
let course: Course = {
name: "Angular",
price: 100,
isFree: true,
};
course.name = "Angular updated name"
course.price = 150; // throws compilation error
price
is a readonly
property in the above code, so you can only provide a value while declaring a variable, you can't modify it afterward. Doing so will result in the following compilation error:
NOTE: In an interface, you can only use the readonly
modifier. You CAN NOT use other modifiers like private
, public
, or protected
inside an interface
.
IS INTERFACE A THING IN JAVASCRIPT?
In JavaScript, the interface is not a thing. They are used to define a type of object or implement it in a class, but only in TypeScript.
TypeScript Interface has zero JavaScript code which means it is only available in TypeScript and does not produce any code in compiled JavaScript files. This is also known as "duck typing" or "structural subtyping".
To understand the second point let's take an example:
In our first blog, we learned that whenever our typescript is compiled, it's converted into JavaScript. Therefore, let us create an interface and write some logic to see what the equivalent code looks like in JavaScript.
Take a look at the following code:
For this, you need a basic project setup. Check out my blog on Introduction to Typescript which includes detailed instructions on how to compile TS files into JS files including the tools and libraries necessary.
Create an index.ts
file and add the following code to it:
interface Course {
name: string;
price: number;
isFree: boolean;
}
let course: Course = {
name: "Angular",
price: 100,
isFree: true,
};
console.log("Created course: ", course);
Then, run the following command to compile the index.ts
into JavaScript:
tsc index.ts
This will generate an index.js
file that contains equivalent JavaScript code. If you open that file, you will see that there is no trace of an interface
. Instead, you will see only a variable declaration and a console log statement. Here is an example of what that would look like:
The reason is that JavaScript cannot understand interfaces. An interface
is a development feature only found in TypeScript. It aims to improve code quality by adding type checks, preventing compilation errors, and ensuring bug-free code.
Some libraries help create interfaces in JavaScript, such as
implement.js
, but if you want them, why not use TypeScript instead?
USING INTERFACES WITH FUNCTIONS:
The use of interfaces in functions generally refers to assigning types to the parameters and explicitly indicating the type of the return value.
INTERFACE FOR FUNCTION PARAMETERS:
To avoid repeating complex types for multiple variables/parameters, we can create an interface for the parameter of the function and use the interface as a type.
Take a look at the following code:
interface Course {
name: string;
price: number;
isFree: boolean;
}
let courses: Course[] = [];
const addCourse = (course: Course): void => {
courses.push(course);
};
addCourse({
name: "Angular",
price: 100,
isFree: true,
});
console.log(courses); // prints [{name: "Angular", price: 100, isFree: true}]
Like our previous examples, we have a Course
interface with some properties. Additionally, we have an addCourse
function that expects a parameter of type Course
. Using addCourse
we append the course
parameter to courses
, a variable that has a type of Course[]
(An array of elements with the type Course
).
When we use interface as a type for the parameter, we avoid writing the same and lengthy type structure over and over again as well as getting autocompletion when we make any operation on the parameter, as shown below:
INTERFACE FOR FUNCTION RETURN TYPE:
The interface can also be used as a function return type, as follows:
interface Course {
name: string;
price: number;
isFree: boolean;
}
let courses: Course[] = [];
const addCourse = (course: Course): void => {
course.name = course.name.toUpperCase();
courses.push(course);
};
// function with return type Course[] (Array of Course)
const getCourses = (): Course[] => {
return courses;
};
addCourse({
name: "Angular",
price: 100,
isFree: true,
});
let coursesArray = getCourses();
A new function called getCourses
is added here to return the courses
array, which is of type Course[]
. This can be specified as a return type.
If you don't explicitly specify the return type of getCourses
, TypeScript will infer it as a Course[]
as follows:
USING INTERFACES WITH CLASSES:
In our last blog, Classes in typescript, we took a deep dive into class concepts.
We will now look at how we can make classes more powerful by using interfaces
.
CLASS IMPLEMENTING INTERFACE:
In TypeScript, a class can implement interfaces to enforce particular contracts (similar to languages such as Java and C#).
To implement an interface inside a class, we use the implements
(with s) keyword after the class name and then specify the interface name. Check out the following code:
interface CourseInterface {
name: string;
price: number;
onPurchase: () => void;
}
class Course implements CourseInterface {
name: string;
price: number;
constructor(name: string, price: number) {
this.name = name;
this.price = price;
}
onPurchase() {
console.log(`You have purchased ${this.name}`);
}
}
As you can see, there is an interface named CourseInterface
that has three properties, which are name
of type string
, price
of type number
, and onPurchase
, a function that accepts no parameters and returns void
.
There is also a class named Course
that implements CourseInterface
. Thus, it is necessary for a Course
class to implement the same mandatory properties and methods that the CourseInterface
possesses. If we miss any one of these mandatory properties, we will receive the following compilation error:
The error reads, Property 'onPurchase' is missing in type 'Course', but it is required in type 'CourseInterface'
. Therefore, we must either add the onPurchase
method or make that property optional.
The interface is somewhat similar to the abstract class, but there are some differences. We'll discuss the difference between an abstract class and an interface in an upcoming post here.
IMPLEMENTING MULTIPLE INTERFACES:
Classes can implement multiple interfaces at once.
Let's take an example to understand this:
interface Shape {
getArea(): number;
}
interface Color {
color: string;
getColor(): string;
}
class Circle implements Shape, Color {
private radius: number;
color: string;
constructor(rad: number, clr: string) {
this.radius = rad;
this.color = clr;
}
getArea(): number {
return Math.PI * this.radius * this.radius;
}
getColor(): string {
return this.color;
}
}
const circle = new Circle(5, "red");
console.log(circle.getArea()); // prints 78.5398
console.log(circle.getColor()); // prints red
The example above shows two interfaces named Shape
and Color
, each of which has certain properties with their types. Additionally, we have a class called Circle
which implements these two interfaces.
If any one of the mandatory properties of any one of the interfaces is missing, we will get the following compilation error:
These interfaces can also be used to implement other classes as follows:
interface Shape {
getArea(): number;
}
interface Color {
color: string;
getColor(): string;
}
class Rectangle implements Shape, Color {
private length: number;
private width: number;
color: string;
constructor(l: number, w: number, clr: string) {
this.length = l;
this.width = w;
this.color = clr;
}
getArea(): number {
return this.length * this.width;
}
getColor(): string {
return this.color;
}
}
const rect = new Rectangle(10, 20, "green");
console.log(rect.getArea()); // prints 200
console.log(rect.getColor()); // prints green
This allows us to add strict type checks and provide documentation in our code, which makes the development process more convenient and reliable.
INTERFACE - INTERFACE INHERITANCE:
Extending an interface
essentially means inheriting it from another interface
. Similar to class inheritance we can use the extends
keyword to inherit an interface.
EXTENDING AN INTERFACE:
Take a look at the following code:
interface Animal {
name: string;
age: number;
eat: () => void;
}
interface Dog extends Animal {
breed: string;
bark: () => void;
}
The above code contains an interface called Animal
and an interface called Dog
which inherits Animal
.
As a result, you can use an interface Dog
to implement a class that must implement Dog
as well as the mandatory properties/methods of the Animal
interface.
Take a look at the following code:
// example for interface inheritance
interface Animal {
name: string;
age: number;
eat: () => void;
}
interface Dog extends Animal {
breed: string;
bark: () => void;
}
class Labrador implements Dog {
name: string;
age: number;
breed: string;
constructor(name: string, age: number, breed: string) {
this.name = name;
this.age = age;
this.breed = breed;
}
bark() {
console.log("Woof! Woof!");
}
eat() {
console.log("Yum yum");
}
}
const labrador = new Labrador("Max", 3, "Labrador");
You will get a compilation error if you miss any of the Animal
or Dog
properties as follows:
In this case, we are missing the eat
property, which is of type () => void
in the Animal
interface.
Also, Animal
can still be used as a valid interface independently if required.
EXTENDING MULTIPLE INTERFACES:
It is possible to extend more than one interface while inheriting. Here is an example:
interface Species {
family: string;
genus: string;
}
interface Animal {
name: string;
age: number;
eat: () => void;
}
interface Dog extends Animal, Species {
breed: string;
bark: () => void;
}
In this case, we have another interface called Species
that is inherited by the Dog
interface. In any class that implements a Dog
interface, we now need to add all the mandatory properties of Species
, Animal
, and Dog
.
DIFFERENCE BETWEEN A TYPE AND AN INTERFACE:
USE CASE:
interfaces
are a way to describe data shapes, such as objects. The type
attribute defines the type of data, for example, union, primitive, intersection, tuple, any, or even object.
EXTENDS & IMPLEMENTS:
As we saw earlier in this blog, we can easily extend and implement interfaces
with TypeScript. However, this is not possible with types
. (Except that the type is representing an object)
For example,
**The following code snippets are valid: **
The following code snippets are invalid:
The error above occurs because ABC
is a union type
, not an object
type
MERGING
When you create multiple interfaces with the same name and some common and some unique properties, TypeScript will merge all the properties and will NOT generate a compilation error as follows:
interface Int1 {
a: number;
b: number;
c: number;
}
interface Int1 {
a: number;
b: number;
d: number;
}
// int1 should include a, b, c and d properties
// if you miss any mandatory property, you will get an error
const int1: Int1 = {
a: 1,
b: 2,
c: 3,
d: 4,
};
// Compiles without an error
However, with types
, TypeScript will throw a compilation error because declaring two types
with the same name is not allowed. Refer to the following image:
DIFFERENCE BETWEEN AN ABSTRACT CLASS AND AN INTERFACE:
Interfaces:
Promote loose coupling (class is dependent on an interface rather than a class for the implementation)
We can implement more than one interface in a class.
Set up a contract between different classes (the syntax/structure for classes to follow)
Serve as a “gatekeeper” to a function (function can enforce the contract)
Work best when we have very different objects that we want to work with together.
Abstract classes:
Strongly couple classes together (class is dependent on another class)
We can't inherit more than one abstract class.
Set up a contract between different classes (the syntax/structure for classes to follow)
Work best when we are trying to build up the definition/blueprint of an object.
CONCLUSION:
In this article, we have written multiple TypeScript interfaces to represent various data structures, discovered how we can use different interfaces together as building blocks to create powerful types, and learned about the differences between normal type declarations and interfaces.
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.
In the last two articles, we opened two useful approaches: blueprints (classes) and contracts (interfaces). You can use both of them together or just one. It is up to you.
You can now start writing interfaces for data structures in your codebase, allowing you to have type-safe code as well as documentation.
Make sure to subscribe to our newsletter on https://blog.wajeshubham.in/ and never miss any upcoming articles related to TypeScript and programming just like this one.
I hope this post will help you in your journey. Keep learning!