Categories
TypeScript

An Introduction to TypeScript Interfaces

Spread the love

The big advantage of TypeScript over plain JavaScript is that it extends the features of JavaScript by adding features that ensure type safety of our program’s objects.

It does this by checking the shape of the values that objects take on. Checking the shape is called duck typing or structural typing. Interfaces are one way to fill the role of naming data types in TypeScript.

They are very useful for defining a contract within our code in TypeScript programs.

In the last part, we’ll look at how to define a TypeScript interface and how to add properties to it. We also look at excess property checks for object literals and defining types for interfaces.

In this article, we’ll look at how to extend interfaces and write interfaces that extend classes.


Extending Interfaces

In TypeScript, interfaces can extend each other just like classes. This lets us copy the members of one interface to another and gives us more flexibility in how we use our interfaces.

We can reuse common implementations in different places and we can extend them in different ways without repeating code for interfaces.

We can extend interfaces with the extends keyword. We can use the keyword to extend one or more interfaces separated by commas. For example, we can use the extends keyword as in the code below:

interface AnimalInterface {
  name: string;  
}
interface DogInterface extends AnimalInterface {
  breed: string;
  age: number;
}
interface CatInterface extends AnimalInterface {
  breed: string;
}

Then, to implement the Dog and Cat interfaces, we have to implement the members listed in the Animal interface as well. For example, we would implement them as in the following code:

interface AnimalInterface {
  name: string;  
}
interface DogInterface extends AnimalInterface {
  breed: string;
  age: number;
}
interface CatInterface extends AnimalInterface {
  breed: string;
}
class Cat implements CatInterface {
  name: string = 'Mary';
  breed: string = 'Persian';
}
class Dog implements DogInterface {
  name: string = 'Jane';
  breed: string = 'Labrador';
  age: number = 10;
}

As we can see, we have added everything from the parent interface and the child interface in our class implementations. We can also extend multiple interfaces as in the following code:

interface MachineInterface {
  name: string;  
}
interface ProductInterface {
  price: number;
}
interface ClockInterface extends MachineInterface, ProductInterface {
  tick(): void;
}
class Clock implements ClockInterface {
  name: string = 'Quartz';
  price: number = 20;
  tick() {
    console.log('tick');
  }
}

As we can see from the code above, we have all the members of the Machineinterface, ProductInterface, and ClockInterface if we implement the ClockInterface as we did with the Clock class.

Note that if we have the same member name in multiple interfaces then they must have identical data types as well. For example, if we have the following code:

interface MachineInterface {
  name: string;  
}
interface ProductInterface {
  name: number;
}
interface ClockInterface extends MachineInterface, ProductInterface {
  tick(): void;
}
class Clock implements ClockInterface {
  name: string = 'Quartz';
  price: number = 20;
  tick() {
    console.log('tick');
  }
}

The Typescript compiler would reject it since we have name being a string in the MachineInterface and name being a number in the ProductInterface.

If we try to compile the code above with the TypeScript compiler, we would get the error:

Interface ‘ClockInterface’ cannot simultaneously extend types ‘MachineInterface’ and ‘ProductInterface’. Named property ‘name’ of types ‘MachineInterface’ and ‘ProductInterface’ are not identical.(2320)

Hybrid Types

We can override the type that’s inferred by an object with the type assertion operator, which is denoted by the as keyword in TypeScript.

This way, we can use code that has dynamic types while we keep using the interface. For example, we can write the following code:

interface Person {
  name: string;
  (name: string): string;
}
function getPerson(): Person {
  let person = (function (name: string) { }) as Person;
  person.name = 'Joe';
  return person;
}
let person = getPerson();
person('Joe');

In the code above, we have the person variable in the getPerson function which we set explicitly with the Person type so that we can assign properties listed in the Person interface to the person variable.


Interfaces Extending Classes

TypeScript interfaces can extend classes. This means that an interface can inherit the members of a class but not their implementation. The class, in this case, acts as an interface with all the members declared without providing the implementation.

This means that when we extend a class with private or protected members, the interface can only be implemented by that class or a sub-class of it. For example, we can write an interface that extends a class as we do in the following code:

class Animal {
  name: string = '';
  private age: number = 0;  
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;  
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';
}

In the code above, we first created the class Animal which has a public member name and a private member age. Then, we added a BirdInterface which extends the Animal class by adding the public members breed and color.

Then, in the Bird class, which extends the Animal class and implements the BirdInterface, we have all the members of the BirdInterface plus the public members of the Animal class.

Since private members can’t be accessed outside of a class, we can’t access the member age in the Bird class. We also can’t add another age member in the Bird class.

Otherwise, we would get the errors:

Class ‘Bird’ incorrectly extends base class ‘Animal’. Property ‘age’ is private in type ‘Animal’ but not in type ‘Bird’.(2415)” and “Class ‘Bird’ incorrectly implements interface ‘BirdInterface’. Property ‘age’ is private in type ‘BirdInterface’ but not in type ‘Bird’.(2420)

However, if we change the age member in the Animal class to a protected member, which can be accessed by all sub-classes that extends Animal, then we can reference it in the Bird class as in the following code:

class Animal {
  name: string = '';
  protected age: number = 0;  
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;    
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';
  age: number = 1;
}

This is the same with methods. Private methods can’t be accessed by anything outside the class that it’s defined in and can’t be overridden by any sub-class or interface. For example, if we have the following code:

class Animal {
  name: string = '';
  private age: number = 0;  
  private getAge() {
    return this.age;
  }
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;   
  getAge(): number;
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';  
  getAge() { return 0 };
}

Then we would get the errors:

Class ‘Bird’ incorrectly extends base class ‘Animal’. Property ‘age’ is private in type ‘Animal’ but not in type ‘Bird’.(2415)” and “Class ‘Bird’ incorrectly implements interface ‘BirdInterface’. Property ‘age’ is private in type ‘BirdInterface’ but not in type ‘Bird’.(2420)

However, we can override protected methods in sub-classes as in the following code:

class Animal {
  name: string = '';
  private age: number = 0;  
  protected getAge() {
    return this.age;
  }
}
interface BirdInterface extends Animal {
  breed: string;
  color: string;     
}
class Bird extends Animal implements BirdInterface {
  name: string = 'Bird';    
  breed: string = 'pigeon';
  color: string = 'Gray';  
  getAge() { return 0 };
}

In TypeScript, interfaces can extend each other just like classes. This lets us copy the members of one interface to another and gives us more flexibility in how we use our interfaces.

We can reuse common implementations in different places and we can extend them in different ways without repeating code for interfaces.

Also, TypeScript interfaces can extend classes. This means that an interface can inherit the members of a class but not their implementation. The class, in this case, acts as an interface with all the members declared without providing the implementation.

This means that when we extend a class with private or protected members, the interface can only be implemented by that class or a sub-class of it.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

Your email address will not be published. Required fields are marked *