What's Object-oriented Programming (OOP)?
Working with "real-life" entities in your code.
Objects >< Classes
Objects are the concrete things we work with in our code. The data structure we use to store data, to store methods and to execute methods on. Class-based creation is an alternative to using object literals!
Classes are blueprints for Objects. Classes allow us to define how objects should look like (which properties and methods they have) => it is easy to build an Object based on its Class (Instance of Class). We can quickly replicate the same object/structure/methods based on the same Classes.
class Department {
name: string; // field of a class
// method = function inside a class
/**
* A function tied to this object/to this class which is executed
* when the object is created
*/
constructor(n: string) {
// property
this.name = n;
}
}
const accounting = new Department('accounting');
console.log(accounting);
// RESULT: JS Object
// Department {name: "accounting"}
// name: "accounting"
// __proto__: Object
es5:
'use strict';
var Department = (function () {
function Department(n) {
this.name = n;
}
return Department;
})();
var accounting = new Department('accounting');
console.log(accounting);
class Department {
name: string;
constructor(n: string) {
this.name = n;
}
describe() {
console.log('Departement: ' + this.name);
}
}
const accounting = new Department('accounting');
accounting.describe();
this
can be tricky:
const accounting = new Department('accounting');
accounting.describe(); // Departement: accounting
const accountingCopy = { describe: accounting.describe };
accountingCopy.describe(); // Departement: undefined
We could prevent it by adding this: Department
to the describe
method.
// adding this: Department to the describe method
describe(this: Department) {
console.log('Departement: ' + this.name);
}
Then accountingCopy
will get a TS error.
// ...
const accountingCopy = { describe: accounting.describe };
accountingCopy.describe();
// The 'this' context of type '{ describe: (this: Department) => void; }' is not assignable to method's 'this' of type 'Department'.
// Property 'name' is missing in type '{ describe: (this: Department) => void; }' but required in type 'Department'.
The solution here, add a name
property to accountingCopy
.
const accountingCopy = { name: 'Whatever', describe: accounting.describe };
accountingCopy.describe();
class Department {
name: string;
employees: string[] = [];
constructor(n: string) {
this.name = n;
}
describe(this: Department) {
console.log('Departement: ' + this.name);
}
addEmployee(employee: string) {
// + validation here (etc.)
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department('accounting');
accounting.addEmployee('Max');
accounting.addEmployee('Manu');
accounting.employees[2] = 'Anna'; // WE DON'T WANT THAT, because we want to use addEmployee (for the validation...)
accounting.describe();
accounting.printEmployeeInformation();
You can properties and methods "private" by adding private
keyword in front of them. The private
properties will be accessible only iniside the class. As a result accounting.employees[2] = 'Anna';
won't be possible.
private employees: string[] = [];
By default, properties and methods are public
. No need to add it public name: string;
.
class Department {
private id: string;
private name: string;
private employees: string[] = [];
constructor(id: string, n: string) {
this.id = id;
this.name = n;
}
//...
The shortcut is:
class Department {
// private id: string;
// private name: string;
private employees: string[] = [];
constructor(private id: string, public name: string) {
// this.id = id;
// this.name = n;
}
//...
class Department {
private employees: string[] = [];
constructor(private id: string, public name: string) {}
describe(this: Department) {
console.log(`Departement (${this.id}): ${this.name}`);
}
addEmployee(employee: string) {
this.employees.push(employee);
}
printEmployeeInformation() {
console.log(this.employees.length);
console.log(this.employees);
}
}
const accounting = new Department('d1', 'accounting');
accounting.addEmployee('Max');
accounting.addEmployee('Manu');
accounting.describe();
accounting.printEmployeeInformation();
If the property won't change (NEVER), add readonly
to it to enforce it.
class Department {
// private readonly id: string;
// private name: string;
private employees: string[] = [];
constructor(private readonly id: string, public name: string) {
// this.id = id;
// this.name = n;
}
//...
Classes are blueprints for JavaScript objects. You define how objects (which are instantiated based on classes) should look like.
Basically a variable in a class.
Private marks properties as "not accessible from outside the class". You ensure that a "private" property is only accessible from inside a class (e.g. from inside a class method).
class Product {
title: string;
price: number;
private isListed: boolean;
constructor(name: string, pr: number) {
this.title = name;
this.price = pr;
this.isListed = true;
}
}
SOLUTION
class Product {
private isListed: boolean;
constructor(public title: string, public price: number) {
this.isListed = true;
}
}
We might need more information for a specific type of department...For example, we have an IT dep which has an id
, a name
and employees
but also administrators
which is something only the IT dep will have. We might have the accounting dep which has an id
, a name
and employees
but also a report
(also specific only to the accounting dep).
Note: YOU CAN ONLY INHERIT FROM ONE CLASS.
class ITDepartment extends Department {
admins: string[];
constructor(id: string) {
super(id, 'IT'); // super is the first to be called before any this
this.admins = admins;
}
}
class ITDepartment extends Department {
constructor(id: string, public admins: string[]) {
super(id, 'IT');
}
}
const it = new ITDepartment('d1', ['Max']);
it.addEmployee('Max');
it.describe();
it.printEmployeeInformation();
// RES
// Departement (d1): IT
// 1
// ["Max"]
Inheritance in action:
class ITDepartment extends Department {
constructor(id: string, public admins: string[]) {
super(id, 'IT');
}
}
class AccountingDepartment extends Department {
constructor(id: string, private reports: string[]) {
super(id, 'Accounting');
}
addReport(text: string) {
this.reports.push(text);
}
printReports() {
console.log(this.reports);
}
}
Let's say we want to override addEmployee
in AccountingDepartment
class. We can't access this.employees
because it is private
in Department
class.
class AccountingDepartment extends Department {
constructor(id: string, private reports: string[]) {
super(id, 'Accounting');
}
addEmployee(name: string) {
if (name === 'Max') {
return;
}
this.employees.push(name); // KO – Property 'employees' is private and only accessible within class 'Department'.
}
//...
If we want to access employees
in class which extends Department
class we should change private
with protected
. As a result, employees
won't be accessible from outside BUT it can be accessed by the class which extend the "parent" class.
A getter:
class AccountingDepartment extends Department {
private lastReport: string;
constructor(id: string, private reports: string[]) {
super(id, 'Accounting');
this.lastReport = reports[0];
}
get mostRecentReport() {
if (this.lastReport) {
return this.lastReport;
}
throw new Error('No report found.');
}
//...
accounting.mostRecentReport // you access it like a property not like you'll call a function
A setter:
//...
set mostRecentReport(value: string) {
if (!value) {
throw new Error('Please pass in a valid value!');
}
this.addReport(value);
}
//...
accounting.mostRecentReport = 'Here is the last report...'; // you call it like a property not like you'll call a function
A static property or static method allows you to add properties / methods which are not accessed on an instance of the class. As a result you don't need to call new
class name first but you can access them directly on the class. Useful for utility functions you want to group or map to a class logically or global const in a class.
Math
object/class is a good example. It is globally available. Math.PI
give you the const PI or Math.pow(2, 2)
give you access to the static method to get the power n of a number. You don't have to call new Math
(and it won't work).
class Department {
static fiscalYear = 2020;
protected employees: string[] = [];
constructor(private readonly id: string, public name: string) {}
static createEmployee(name: string) {
return { name: name };
}
//...
const employee1 = Department.createEmployee('Max');
console.log(employee1, Department.fiscalYear);
Note: you you add a static
property or method, you can't access them from your non static parts.
//...
printEmployeeInformation() {
console.log(this.employees);
console.log(this.fiscalYear); // KO – Property 'fiscalYear' is a static member of type 'Department'
console.log(Department.fiscalYear); // OK
}
//...
Because this
does refer to the instance created based on the class. The static property is not available on the instance because the all idea behind static
property or method is to be detached of the instance. You need to access it via the name of the class: Department.fiscalYear
.
You can easily override a method in a class which extends
another one.
class AccountingDepartment extends Department {
//...
describe() {
console.log('Accounting Department – ID: ', this.id); // note: we need to change id to protected in Department
}
//...
But sometimes you don't want just to offer the option to override a method. You instead want to force the dev working on an extending class to implement or to override a certain method.
abstract class Department {
//...
abstract describe(this: Department): void;
Note: an abstract class
cannot be instantiated themselves. It is only useful for the inheriting class (to be forced to implement specific methods).
The singleton pattern restricts the instantiation of a class and ensures that only one instance of the class exists. Singleton pattern restricts the instantiation of a class and ensures that only one instance of the class exists.
For example, we know there is only one accounting dep. We can make the construtor private
– private constructor(...)
. It ensures we can't call new
on the class, so it is only accessible inside the class.
class AccountingDepartment extends Department {
private lastReport: string;
// a private constructor ensures we can't call new on the class
private constructor(id: string, private reports: string[]) {
super(id, 'Accounting');
this.lastReport = reports[0];
}
//...
The solution to get access to a private
constructor is to use a static
method.
class AccountingDepartment extends Department {
//...
private static instance: AccountingDepartment; // we create a private static field
private constructor(id: string, private reports: string[]) {
super(id, 'Accounting');
//...
}
// we create a static method
static getInstance() {
if (AccountingDepartment.instance) { // we check if the "instance" exists
return this.instance; // if yes, we return it
}
this.instance = new AccountingDepartment('d2', []); // Otherwise, we call the private constructor, to create the new instance
return this.instance;
}
//...
const accounting1 = AccountingDepartment.getInstance();
const accounting2 = AccountingDepartment.getInstance();
// accounting1 and accounting2 will be the same object / the same instance
A method you call directly on a class, not on an object created based on it.
Inheritance allows you to share some common functionality and yet create more specialized blueprints. You can set up a base class and then create more specialized classes that inherit from it.
A class that can't be instantiated but has to be extended. Abstract classes can't be instantiated.
You only ever have one instance of a singleton class. A singleton class is configured such that you don't create it with "new" but by calling a method which then ensures that only one instance of the class exists at any given time.
In its simple version, an interface describes the structure (and the the concrete values) of an object. We can use to describe how the object should look like. We don't assign any value to it. We can use it as a type to type check for object which have to get that structure.
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
let user1: Person;
user1 = {
name: 'Max',
age: 30,
greet(phrase: string) {
console.log(`${phrase} from ${this.name}`);
},
};
console.log(user1);
console.log(user1.greet('Hi there –'));
What is the ≠ between interface
and type
?
// INTERFACE
interface Person {
name: string;
age: number;
greet(phrase: string): void;
}
// VS
// TYPE
type Person = {
name: string;
age: number;
greet(phrase: string): void;
};
You can implement an interface in a class. An interface can be used as a contract, a class can implement.
interface Greetable {
name: string;
greet(phrase: string): void;
}
class Person implements Greetable {
name: string;
constructor(n: string) {
this.name = n;
}
greet(phrase: string) {
console.log(`${phrase} from ${this.name}`);
}
}
let user1: Greetable; // OK
user1 = new Person('Max'); // OK
Note: YOU CAN ONLY INHERIT FROM ONE CLASS BUT YOU CAN IMPLEMENT MORE THAN ONE INTERFACE.
An interface is a bit like an abstract class. But the main difference is interface doesn't have any details about the implementation at all. And the abstract class has a mix between how the object should like and implementation as well.
You can add readonly
to a property in an interface. Only be set once and it is readonly after => immutable.
interface Greetable {
readonly name: string;
greet(phrase: string): void;
}
You can also implement inheritance in interfaces.
interface Named {
readonly name: string;
}
interface Greetable {
greet(phrase: string): void;
}
class Person implements Greetable, Named {
//...
interface Named {
readonly name: string;
}
interface Greetable extends Named {
greet(phrase: string): void;
}
class Person implements Greetable {
An interface can inherit from more than one interface (≠ than for class).
interface Greetable extends Named, AnotherInterface {...}
// TYPE
type AddFn = (a: number, b: number) => number;
// INTERFACE (alternative)
interface AddFn {
(a: number, b: number): number; // no method name, same as above
}
let add: AddFn;
add = (n1: number, n2: number) => {
return n1 + n2;
};
interface Named {
readonly name?: string; // optional name in interface
outputName?: string; // optional property
myMethod?(): void; // optional method
}
//...
class Person implements Greetable {
name?: string; // optional name in class
constructor(n?: string) { // n?
if (n) { // check if n exists
this.name = n;
}
}
In es5 we can't see any reference to any of our interface. JavaScript doesn't know the interface concept. We can use it to improve our code but there is no output for the interface. Pure dev feature, to better code, to improve the structure and readibility. At runtime, no trace of interface.
Interfaces can't be instantiated and are not compiled, classes can be instantiated and are compiled. Interfaces are a pure TS feature and can't be instantiated.
You want to store a union type. Interfaces describe objects (or function types) but can't store/ describe arbitrary types like union types.