Welcome! Today we’re going to be brushing up on TypeScript classes, interfaces, inheritance, and other object-oriented programming (OOP) concepts.
While TypeScript and JavaScript might not be the first languages you think of when it comes to object-oriented programming, you’d be surprised to know just how much support there is for building out complex, robust components in an object-oriented style. The latest versions of TypeScript and JavaScript both introduced changes to their syntax to make it easier for OOP to take place.
By popular demand, we’ll be focusing specifically on OOP concepts in TypeScript today. We’ll start with a basic rundown of what classes are, and then move on to concepts like encapsulation, class inheritance, interfaces, and more!
By the end, you should have a basic understanding of how to implement various OOP concepts in TypeScript.
Try one of our 300+ courses and learning paths: TypeScript for Programmers.
Before the release of ECMAScript 6 (ES6), JavaScript was primarily a functional programming language where inheritance was prototype-based. When the syntax for supporting classes was introduced in 2015, TypeScript quickly adapted to take advantage of object-oriented techniques like encapsulation and abstraction.
TypeScript and JavaScript classes are comprised of the following:
Classes provide a fundamental structure for creating reusable components in JavaScript. They are an abstraction in object-oriented programming (OOP) languages used to define objects, and pass down properties and functions to other classes and objects.
Objects are data structures made by encapsulating data, and the methods that work on that data. So, you can think of JavaScript classes as not being objects in the truest sense, but more similar to a blueprint for objects.
Note: TypeScript fully supports the class syntax that was introduced in 2015 with the release of ECMAScript 6 (ES6).
Class declarations are used to define a class using the class
keyword along with the class name, and curly braces ‘{}’.
class Fruit {// this is an empty class}
Class expressions are another way to define a class, but they can be named or unnamed.
let Fruit = class {// this class is unnamed}let Fruit = class edible_fruits {// this class is named}
You can access named class expressions using the name
keyword.
let Fruit = class edible_fruits {// this class is named}console.log(Fruit.name);// returns "edible_fruits"
A key concept of object-oriented programming is encapsulation. Encapsulation entails restricting access to an object’s state by enclosing data and methods into one unit. Restricting access to certain data or components can be useful for preventing outside code from calling private methods within a specific class.
TypeScript facilitates encapsulation by enclosing data and its related methods within a class.
For example, if you have a ‘class Student’, with two data elements, and a method, you can encapsulate them using the following syntax:
class Student {name: string=''roll: number = 0getRoll(): number{return this.roll}}
let fruit_one = "Apple";
Objects are also variables, but instead of a single value, they can be assigned multiple values written as key : value pairs. The collection of these key : value pairs make up different properties of the object they belong to.
const fruits = {name: "Apple",color: "red",variety: "Fuji"};
To access class members (data or methods) we have to create an instance of their object.
In the following example, there are two classes, Student
, and School
. Using the object of class School
, we can try to access a data member of the ‘Student’ class with uni.roll
.
Doing so will return a warning that the desired data does not exist in class School
. Instead, we can fix the code by commenting uni.roll
and uncommenting student.roll
.
Try it out yourself!
// Student classclass Student {name: string = ''roll: number = 0getRoll(): number {return this.roll}}// School classclass School {name: string =''location: string = ''}// Create objects of each class.const student = new Student()const uni = new School()// Returns Warning: Property 'rule' does not exist on type 'School'uni.roll = 5;// The following code is correct.// Comment the above code and uncomment the following// student.roll;
A class can have a special method or function known as a ‘constructor’ that gets called automatically when we create an object of that class. A constructor can be used to initialize the class data or perform other actions upon instantiation of its objects. However, it’s not necessary for a class to include a constructor.
Below, the example demonstrates a class ‘Car’ with a public constructor that is automatically called upon object instantiation. At the same time, the constructor creates an instance of the class.
class Car {// define propertiesmakeAndModel: string;year: number = 0// constructor of Carpublic constructor() {this.makeAndModel = 'Toyota Corolla'this.year = 2015}}// create an object of the classconst car = new Car()console.log("\n\n Car make and model : " + car.makeAndModel)console.log("\n\n Year this " + car.makeAndModel + " was manufactured: " + car.year)
Another key concept of object-oriented programming that TypeScript supports is inheritance. Inheritance allows us to derive a class from another (parent or super) class, thus extending the parent class’s functionality. The newly created classes are referred to as child or sub classes.
Child classes inherit all properties and methods from their parent class, but do not inherit any private data members or constructors.
You can use the extends keyword to derive child classes from parent classes.
class child_class extends parent_class
There are three types of class inheritance:
An access modifier restricts the visibility of class data and methods.
TypeScript provides three key access modifiers:
public
private
protected
All class members in TypeScript are public by default, but can otherwise be made public using the public
keyword. These members can be accessed anywhere without restriction when no modifier is specified.
A public method or data of a class can be accessed by the class itself or by any other (derived or not) class.
class Fruit {public fruit_plu: number = 4129;fruit_name: string}let apple = new Fruit();apple.fruit_plu = 4129;apple.fruit_name = "Fuji Apple";console.log ("\n\n The PLU code for a " + apple.fruit_name +" is " + apple.fruit_plu)
In the example above, ‘fruit_plu’ and ‘fruit_name’ keywords are both considered to be public class members even though ‘fruit_plu’ is the only one with an access modifier in front of it. Remember, when you don’t specify the scope of a class member, it can be accessed from outside of the class.
A private
method or data member of a class cannot be accessed from outside of its class. You can use the private
keyword to set its scope. When the scope of a private member is limited to its class, only other methods in that same class can have access to it.
class Fruit {private fruit_plu: number = 4129;fruit_name: string}let apple = new Fruit();// running this console.log should return an error because it is inaccessibleconsole.log("\n\n The PLU code for this fruit is " + apple.fruit_plu)
In the example above, attempting to access the fruit_plu
member will return a compiler error because its scope has been set to private
. You will still be able to access the fruit_name
member because its scope is by default, public
.
A protected access modifier works similarly to the private access modifier, with one major exception. The protected methods or data members of a class can be accessed by the class itself and also by any child class derived from it. With TypeScript, you can use the constructor keyword to declare a public property and a protected property in the same class. These are parameter properties and can allow you to declare a constructor parameter and a class member at the same time.
Note: One caveat of TypeScript’s type system is that the private and protected scopes are only enforced during runtime type checking.
Now that you have a basic understanding of inheritance and access modifiers, we can demonstrate how these two concepts can be used to modify access to some members of a class but not others.
In the following example, you’ll see that the scope of all class members is readily accessible. You should be able to execute this program without returning any errors.
However, we have also included code that alters the scope of different class members as comments. Try uncommenting different sections of the code to see how access is enforced by different access modifiers! You’ll also get to see what kind of errors are returned by TypeScript.
//// Base classclass Car {protected makeAndModel: string;private year: number = 0// constructor of Carpublic constructor() {}//getter of make and modelpublic getMakeAndModel (): string {return this.makeAndModel;}//setter of make and modelpublic setMakeAndModel(make: string) {this.makeAndModel = make;}//getter of manufacture yearpublic getYear (): number {return this.year;}//setter of manufacture yearpublic setYear(year: number) {this.year = year;}}// Derived classclass Tesla extends Car {//public data member, for the car location.public location: string//constructor of Tesla classconstructor() {super();super.makeAndModel = 'Tesla X'}/** Uncommenting the following will give error, because *//** year is defined private (instead of public or protected) *//*getYear (): number {return this.year;}*/}// create objects of each class.const tesla = new Tesla()const car = new Car()//setYear is public hence can be accessed from derived classtesla.setYear(2022)tesla.location = 'New York'console.log("\n\nMake and model of car: " + tesla.getMakeAndModel())console.log("\n\nLocation: "+ tesla.location);// Uncommenting the following code should give error because//location is first defined in the derived class//car.location = "San Francisco"// Uncommenting the following code should give error because//year is a private variable.//tesla.year = 2001;
There are two ways to define types for your data in TypeScript: type aliases and interfaces.
Type aliases are declared using the type
keyword, and are used to explicitly annotate a type with a name (alias). Type aliases can be used to represent primitive data types like string or boolean, but they can also be used to represent object
types, tuples
, and more!
Unlike interfaces, type aliases cannot be declared more than once, and cannot be changed after being created.
Note: Fun fact! The TypeScript compiler converts TypeScript entirely into JavaScript code during a process called transpilation. This is to ensure that any and all JavaScript programs are fully compatible with the TypeScript programming language.
Interfaces are another way to define the data structure of your objects. It is an abstract type that tells the TypeScript compiler which properties a given object can have. In TypeScript, interfaces provides the syntax for an object to declare properties, methods, and events, but it’s up to the deriving class to define those members. Unlike type aliases, you can freely add new fields to an existing interface.
You can declare an interface using the interface
keyword. In this example, we can define an interface for an apple with the properties ‘variety’ and ‘color’ as strings.
interface Apple {variety: string;color: string}
To implement the Apple
interface, we simply assign values to the properties.
interface Apple {variety: string;color: string}let newFruit: Apple = {variety: "Opal",color: "yellow",};console.log("\n\n We have a new type of apple in stock! It's " + newFruit.color + " and of the "+ newFruit.variety + " variety.")
Interfaces can be thought of as a blueprint for the data structure that deriving classes must follow. In the example below, we’ll look at how to implement an interface with a class using the implements
keyword.
// Interface as a blueprintinterface IStudent {roll: numberstdName: string// ? implies the data item is a derived class// may or may containstdDOB?: string// Method declaration (no body) in the interfacegetFathersName():string;}// A class implementing interfaceclass BSStudent implements IStudent {roll: numberstdName: stringfathersName: stringgetFathersName():string {return this.fathersName}}// Create an object of the class.const std = new BSStudent ()std.fathersName = 'Bob'std.roll = 33std.stdName = "John"console.log("\n\n Student's name = "+std.stdName)console.log("\n\n Father's name = "+std.fathersName)console.log("\n\n Roll number = "+std.roll)
Just like with classes, we can derive an interface from other interfaces through inheritance. Unlike classes, a single interface may be extended from multiple interfaces.
In the example below, we demonstrate how the interface ITeacherAndStudent
can be derived from the IStudent
and ITeacher
interfaces.
// Interface for studentsinterface IStudent {roll: numberstdName: stringgetFathersName():string;}// Interface for teachersinterface ITeacher {id: numbername: string}// An interface derived from both students and teachers.interface ITeacherAndStudent extends IStudent, ITeacher {age: number}
Try one of our 300+ courses and learning paths: TypeScript for Programmers.
Great job! By now, you should have a basic understanding of TypeScript classes, interfaces, and how to use them alongside other object-oriented programming concepts. That said, you don’t have to stop learning quite yet. TypeScript is a powerful tool for making JavaScript easier for collaborative efforts. There is a huge library of fantastic resources that are available for developers of all levels.
To help you master TypeScript, we’ve created the TypeScript for Programmers learning path to help you build advanced TypeScript programming skills.
Happy learning!
Free Resources