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.