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.
We'll cover:
- What is a class?
- Declare a TypeScript class
- Encapsulation with classes
- Class inheritance
- Access modifiers
- Inheritance with access modifiers
- Interface and type aliases
- Wrapping up and next steps
What are classes?
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:
- Constructors
- Properties
- Methods
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).
Declare a TypeScript class
Class declarations are used to define a class using the class
keyword along with the class name, and curly braces '{}'.
The class
keyword:
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"
Encapsulation with Classes
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 = 0
getRoll(): number{
return this.roll
}
}
Class objects
In JavaScript, objects are variables that can hold a single value.
The variable fruit_one has been assigned a string value of "Apple":
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.
A collection of key:value pairs:
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 out a class object for yourself on the original article on Educative!
Class constructors
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 properties
makeAndModel: string;
year: number = 0
// constructor of Car
public constructor() {
this.makeAndModel = 'Toyota Corolla'
this.year = 2015
}
}
// create an object of the class
const 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)
----> Car make and model : Toyota Corolla
Year this Toyota Corolla was manufactured: 2015
Class inheritance
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.
Syntax
class child_class extends parent_class
There are three types of class inheritance:
- Single: When one child class inherits class properties and methods from one parent class.
- Multiple: When a child class inherits class properties and methods from more than one parent class. TypeScript does not support multiple inheritances.
- Multi-level: When a class inherits class properties and methods from another child class (like a grandchild or great-grandchild).
Access modifiers
An access modifier restricts the visibility of class data and methods.
TypeScript provides three key access modifiers:
public
private
protected
Public modifiers
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.
Public and default values:
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)
----> The PLU code for a Fuji Apple is 4129
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.
Private modifiers
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 inaccessible
console.log("\n\n The PLU code for this fruit is " + apple.fruit_plu)
----> index.ts(9,60): error TS2341: Property 'fruit_plu' is private and only accessible within class 'Fruit'.
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
.
Protected modifiers
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.
Inheritance with access modifiers
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. Visit the original article on Educative to practice 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 class
class Car {
protected makeAndModel: string;
private year: number = 0
// constructor of Car
public constructor() {
}
//getter of make and model
public getMakeAndModel (): string {
return this.makeAndModel;
}
//setter of make and model
public setMakeAndModel(make: string) {
this.makeAndModel = make;
}
//getter of manufacture year
public getYear (): number {
return this.year;
}
//setter of manufacture year
public setYear(year: number) {
this.year = year;
}
}
// Derived class
class Tesla extends Car {
//public data member, for the car location.
public location: string
//constructor of Tesla class
constructor() {
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 class
tesla.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;
----> Make and model of car: Tesla X
Location: New York
Interfaces and type aliases
Type Aliases
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
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 provide 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.")
----> We have a new type of apple in stock! It's yellow and of the Opal 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 blueprint
interface IStudent {
roll: number
stdName: string
// ? implies the data item is a derived class
// may or may contain
stdDOB?: string
// Method declaration (no body) in the interface
getFathersName():string;
}
// A class implementing interface
class BSStudent implements IStudent {
roll: number
stdName: string
fathersName: string
getFathersName():string {
return this.fathersName
}
}
// Create an object of the class.
const std = new BSStudent ()
std.fathersName = 'Bob'
std.roll = 33
std.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)
----> Student's name = John
Father's name = Bob
Roll number = 33
Interfaces and inheritance
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 students
interface IStudent {
roll: number
stdName: string
getFathersName():string;
}
// Interface for teachers
interface ITeacher {
id: number
name: string
}
// An interface derived from both students and teachers.
interface ITeacherAndStudent extends IStudent, ITeacher {
age: number
}
Wrapping up and next steps
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!
Continue reading about TypeScript on Educative
- TypeScript enum guide: Get started in 5 minutes
- TypeScript Tutorial: A step-by-step guide to learn TypeScript
- Understanding Advanced Concepts in TypeScript
- A Simple Guide to TypeScript Interfaces: declaration & use cases
Start a discussion
What do you hope to do with TypeScript? Was this article helpful? Let us know in the comments below!
Top comments (0)