Interfaces in TypeScript

Interfaces in TypeScript

Play this article

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.

image.png

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:

image.png

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.

image.png

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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:

image.png

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: **

image.png

The following code snippets are invalid:

image.png

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:

image.png

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!

My Website, connect with me on LinkedIn and GitHub