Typescript explained in Javascript: extends
2nd post in "TypeScript explained in JavaScript" series
Spencer Miskoviak
May 17, 2020
Photo by Oliver Dumoulin
The first post covered the keyof
operator
and I would recommend starting there if you haven't. In that post, the extends
keyword was used to demonstrate one of the uses for the keyof
operator. This
post will cover the extends
keyword in more depth.
JavaScript
JavaScript has supported the
extends
keyword with class declarations since ES6. It can be used to create a subclass
or child class of another class.
For example, let's say we create a simple Book
class to represent the concept
of a book.
class Book {
constructor(title) {
this.title = title;
}
}
This Book
class accepts a book's title when it's instantiated. For example,
we can instantiate a class for the book "The Phoenix Project."
const book = new Book("The Phoenix Project");
Now, say we want to not only represent a book, but also the concept of an audio book. You could say an audio book is an extension of a book. It still has a title, but it also has a total duration; the length of time it takes to listen to the entire audio book.
class AudioBook extends Book {
constructor(title, duration) {
super(title);
this.duration = duration;
}
describe() {
return `"${this.title}" is ${this.duration} minutes`;
}
}
This AudioBook
class not only accepts a book's title, but also the total
duration. To demonstrate a bit more functionality, a method was also
added to quickly describe the book. For example, we can instantiate a class for
the audio book "Educated: A Memoir" and denote that it is 12 hours and 11 minutes
long (or 731 minutes).
const book = new AudioBook("Educated: A Memoir", 731);
console.log(book.describe()); // "Educated: A Memoir" is 731 minutes
We may also consider creating an EBook
class to represent digital books that
could be read on tablets.
In this context, the extends
keyword is useful in JavaScript when one concept
needs to be applied to another and they need to remain consistent. For example,
now if the author
is added to the Book
class, it can be easily adapted to the
AudioBook
class.
TypeScript
How does this apply to TypeScript?
Classes
The first use case is identical to above. Since TypeScript is a superset of
JavaScript, it also supports usage of the extends
keyword with class
decelerations. Minus a few missing types, the above code is also valid TypeScript.
However, TypeScript also leverages this concept of "extending" one thing to another in the type system. Notably, interfaces and generics (as constraints).
Interfaces
Let's start with interfaces. If we were to create an interface for the Book
class from above to implement it would look like the following.
interface Book {
title: string;
}
Similarly, we can declare an interface for the AudioBook
class.
interface AudioBook extends Book {
duration: number;
describe(): string;
}
However, this interface is extending the Book
interface. This behaves
conceptually the same way as extending a class in JavaScript. The AudioBook
interface extends the Book
interface (inheriting the title
property) and adds on
two additional properties.
The resulting interface is equivalent to the following.
interface AudioBook {
title: string;
duration: number;
describe(): string;
}
Generics and Constraints
One of the other uses for the extends
keyword in TypeScript is constraints
for generics (this concept also applies directly to conditional types which
will be covered in an upcoming post in this series).
For example, let's say we want to define a printTitle
function that expects
any type of Book
(this could also be an AudioBook
or EBook
).
function printTitle(book: any) {
console.log(book.title);
}
const book = new Book("The Phoenix Project");
printTitle(book); // The Phoenix Project
const audioBook = new AudioBook("Educated: A Memoir", 731);
printTitle(audioBook); // Educated: A Memoir
printTitle("The Phoenix Project"); // undefined
This works mostly as expected. However, the type for book
is declared as any
.
This isn't ideal, because the last example is not a valid book. It's passing
in a string directly which doesn't have a title
property, so it prints
undefined
. Ideally, this type of issue is caught at compile time, not run
time.
Since this is a "generic" function in the sense that it can accept any
kind of book, let's define a generic Input
to represent that.
function printTitle<Input>(book: Input) {
console.log(book.title);
}
Unfortunately, this isn't valid and we'll see the TypeScript compiler error with:
Property 'title' does not exist on type 'Input'
. So how do we say that the
generic Input
type has a title
property but still allow anything with
additional properties?
We can apply a constraint, or say the Input
type must extend the Book
interface:
function printTitle<Input extends Book>(book: Input) {
console.log(book.title);
}
const book = new Book("The Phoenix Project");
printTitle(book); // The Phoenix Project
const audioBook = new AudioBook("Educated: A Memoir", 731);
printTitle(audioBook); // Educated: A Memoir
printTitle("The Phoenix Project");
// Argument of type '"The Phoenix Project"' is not assignable to parameter of type 'Book'
This is exactly what we want. The printTitle
function accepts anything with a
title
attribute, and now results in a compile time error for invalid usages.
One important thing to note here is that TypeScript relies on types being
structurally equivalent.
This means that the input to printTitle
doesn't have to literally extend Book
,
but rather have the same structure as the Book
interface. The following
object literal with a title
property will also be valid:
printTitle({ title: "The Phoenix Project" });
Definition
The extends
keyword has a single behavior (extending things), but it useful in
several contexts:
Interestingly, an interface can even extend a class! 🤯
Conclusion
In summary, TypeScript's extends
is equivalent to JavaScript's extends
when
working with classes. However, TypeScript's extends
is "overloaded" with
additional functionality that is particularly useful when working with types.
Similar to the keyof
operator, I would consider extends
to be a fundamental
building block for some of the more complex and powerful features of TypeScript,
specifically conditional types that will be covered in a future post.
Tags:
course
Practical Abstract Syntax Trees
Learn the fundamentals of abstract syntax trees, what they are, how they work, and dive into several practical use cases of abstract syntax trees to maintain a JavaScript codebase.
Check out the course