DEV Community

loading...
Cover image for Object Oriented TypeScript

Object Oriented TypeScript

Kinanee Samson
I am a frontend web designer based in Nigeria, I am a determined and sarcastic person with a wicked sense of humor.
・9 min read

Object Oriented Programming is a software design pattern where objects related to the problem being solved are modeled inside our code.

I really like this approach to programming because it fits so much into real life around us. We have a class which is a template for defining the properties and methods available to instances of that class. Since TypeScript is a super-set of JavaScript, you should know that deep underneath all that syntatic sugar, JavaScript uses prototypes and not classes.

Object Oriented Programming in TypeScript is quite similar to Object Oriented JavaScript especially when written in ES6. But TypeScript like normal brings more features to the table. In TypeScript we have a lot of data structures and tools that will allow us to achieve true Object Oriented Programming. TypeScript extends JavaScript in OOP by providing interfaces, decorators, access modifiers, static typing and more, with TypeScript we can achieve all the tools of OOP e.g polymorphism, inheritance, encapsulation, delegation, classes, and more.

Classes

A class serves as a template for defining what an object will look like properties and what it can do methods. A class is declared with the class keyword followed by the name of the class and it's a good convention to always begin your class names with Uppercase for the first letter. A class can have any number of properties we see fit and any number of methods we also see fit. Every class can implement it's own custom Constructor function that will define how instances of that class are created. If the class is inheriting from another class, the base class would just call it's parent's constructor function.

class _Element {
    constructor(){}
}

let hydrogen = new _Element()
Enter fullscreen mode Exit fullscreen mode

Above we have created a very basic class _Element which will define what an element should look like and what it can do. We proceeded to create an instance of that element hydrogen by calling the new keyword followed by the name of the class and we invoke it like a function. The constructor function defined on the class would run only once, creating a new instance of an Element. Our Element has no properties on the but we can add some properties in the constructor function and annotate the variable name with an access modifier that will determine who has access to that property. TypeScript will auto add any argument we annotate with an access modifier to a property on the class, this helps to keep our code short and clean.

class _Element {
    constructor(
      private _name: string,
      readonly atomicMass: number,
      protected _symbol: string
    ){}
}

let hydrogen = new _Element('Hydrogen', 1, 'H')
hydrogen.atomicMass // 1
Enter fullscreen mode Exit fullscreen mode

By annotating each argument passed into the constructor function with an access modifier and taking advantage of TypeScript's static typing by explicitly typing each argument to be of a particular type. This ensures that the right type of value is actually passed for each property when we create a new instance of that class. We can add methods to it and also define getters and setters. In real instance you should declare your properties to be private, this helps make code easier to debug. If a property is private on a class, we can only get access to that property from inside the class or inside an instance of the class. This is where getters and setters come in handy.

class _Element {
    constructor(
      private _name: string,
      readonly atomicMass: number,
      protected _symbol: string
    ){}

    get name() {
        return this._name
    }

    set name (name){
        this._name = name
    }
}

let hydrogen = new _Element('Hydrogen', 1, 'H')
hydrongen._symbol // Not okay
hydrogen.name = 'something else' // Okay
Enter fullscreen mode Exit fullscreen mode

When we say a property is public we can access it from anywhere in our code. When we mark it with readonly, we are saying that other parts of the code are going to be able to see the value but they can't change it. We can also use protected, this is quite similar to private. For using getters the main property has to begin with an underscore _varName while the getter/setter for that property will be the name of the property but without the underscore.

Inheritance

Inheritance is fully supported by TypeScript, inheritance is a principle of OOP that allows us to extend our classes, we can basically create a sub class by extending from a parent class. The sub class will inherit all of the properties and methods declared on the parent class. This helps us to express relationships between objects using the is a pattern. This will also give rise to polymorphism which we will discuss in a bit.

When we create a child class we can define a custom constructor function or we can just call the constructor function of the parent class.

// Extending with Parent's constructor function
class Molecule extends _Element {
}

let water = new Molecule('H2O', 18, 'Hydrogen Oxide');

water._symbol // NOT POSSIBLE private
water.name  // 'Hydorgen'
water.name = 'Water'
Enter fullscreen mode Exit fullscreen mode

And this is how cool using TypeScript can be, we just extended the Element class and we called the constructor function by calling super and spreading in the arguments we pass in. We can also define a custom constructor function which we will do below, however we need to call the parent class constructor function and pass in the right variables.

// Extending with custom constructor function
class Molecule extends _Element {
    constructor(
        _name:string,
        atomicMass: number,
        _symbol: string,
        public reactants: _Elements[]
    ) {
            super(_name, atomicMass, _symbol)
        }

    getReactants(){
        let reactants = this.reactants.map(el => {
            el._name
        })
        return reactants.join(' ')
    }
}

let hydrogen = new _Element('H', 2, 'Hydrogen');
let oxygen = new _Element('O', 16, 'Oxygen')

let Water = new Molecule('H20', 18, 'Water', [hydrogen, oxygen])

Water.getReactants() // Hydrogen Oxygen

Enter fullscreen mode Exit fullscreen mode

Polymorphism

Polymorphism a greek word which translates to "having many forms" is principle of OOP that allows us to customize the behavior of sub classes. We can have a method that does something on a base/Parent class, but we want the child class to implement the method in quite a different from the parent, this is where polymorphism comes in handy.

Let's say we have a class automobiles and we know that all automobiles can move. However the way a plane moves is different from a car right? same-thing with a boat and a car. The parent class is the automobiles that defines that all automobiles can move. A plane or a boat is the sub class and they can have their own implementation of how they move.

Polymorphism is a feature that is fully supported by TypeScript, one of the primary tools for achieving polymorphism let's see polymorphism in action then we will look at interfaces and how they help with polymorphism.

class Automobiles {
  constructor(private _wheels: number, private _type: string) {}

  move() {
    return `moving in it's own way`;
  }
}
Enter fullscreen mode Exit fullscreen mode

We have defined a dummy automobile class, pay no attention to how simple it looks we are focused on polymorphism here. Let's define a child class to extend Automobiles and define how it moves.

class Car extends Automobiles {
    constructor(private _wheels: number, private _type: string){}

    move(){
        return `This car is moving in it's own way`
    }
}

let mercedes = new Car(4, 'sports car')

console.log(mercedes.move()) // This car is moving in it's own way
Enter fullscreen mode Exit fullscreen mode

Pay no attention to how simple the move method is, the idea is to show you that you can define an entirely different logic for the move method, however i think it makes sense if both methods should return the same thing. That's one of the reason i returned a string inside the Car class. Some times i think it actually pays to work with interfaces rather than extending classes. This does not mean in any that using classes is wrong. You can still do that but let's look up interfaces.

Function Signatures

Polymorphism can also occur in the form of a function that implements an interface.

interface Person {
  name:string,
  age: number,
  gender: string
}

type createPersonSignature = (name:string, age:number, gender:string) => Person;

let createPerson: createPersonSignature
let createAdmin: createPersonSignature

createPerson = (name:string, age: number, gender:string) => ({
  name,
  age,
  gender,
  admin: false
})

createAdmin = (name: string, age: number, gender: string) => {
  console.log('creating admin')
  return { name, age, gender, admin: true}
}

let sam = createPerson('sam', 30, 'male')
let superAdmin = createAdmin('super', 100, 'female')
Enter fullscreen mode Exit fullscreen mode

Objects

Polymorphism can also be achieved in TypeScript by using an object that implements an interface, this is handy if you like working with object literals.

interface Person {
  name: string
  age: number
}

let userOne: Person 
let userTwo: Person

let makeName = (name:string) => name

userOne = {
  name: 'sam',
  age: Math.random()
}

userTwo = {
  name: makeName('john'),
  age: 25
}
Enter fullscreen mode Exit fullscreen mode

Interfaces

An interface is simply a contract. It is a contract because all clients that implements it must adhere to the rules defined in the interface. A client is simply a class or object that implements the interface. An interface acts like a type definition for a classes to ensure that they have the right shape an structure. Normally on an interface we can define properties and methods they way we do on types.

interface AutomobileContract {
    move: ()=> string,
}
Enter fullscreen mode Exit fullscreen mode

We have defined a simple automobile interface, we just declared a simple method the move method and it returns a string, now rather than having a base class and a sub class we can have classes that implements the method where the functionality is required.

class Car implements AutomobileContract {
    constructor(private _wheels: number, private _type: string){}

    move(){
        return `This car is moving in it's own way`
    }
}
Enter fullscreen mode Exit fullscreen mode

I think this way of doing things makes thing much easier and cleaner. I don't need to be bothered about a complex tree of hierarchy. Instead you just have to look in one place. To implement a class you have to use the implements keyword and a class can implement more than one interface but on the flip side the class has to explicitly fulfill the terms of the contract. If we didn't have the move method on the car class it would show an error in our IDE. Even if we implemented the function and it didn't return a string there would still be an error in our IDE.

Interfaces can extend from other interfaces a class. If an interface extends a class, it's contract will be such that it defines the methods and properties on the class including the types and function signature on that class. Let's see typical example of an interface extending from a class.

// EXTENDING FROM A CLASS
class Book {
  constructor(public title: string, public author:string){}

  getTitle(){
    return this.title
  }
}

interface TextBookContract extends Book {
  subject: string
}

let chemistryTextBook: TextBookContract;

chemistryTextBook = {
  title: 'chemistry book',
  author: 'John Doe',
  subject: 'chemisty',
  getTitle: function () {
    return `${this.title}`
  }
}
Enter fullscreen mode Exit fullscreen mode

An interface can also extend from another interface and let's see an example of that.

interface BookContract {
  title: string;
  pages: number;
  author: string;
}

interface NovelContract extends BookContract {
  genre: string;
}

interface TextBookContract extends BookContract {
  subject: string;
}

let Book1: TextBookContract = {
  title: "chemistry book",
  pages: 380,
  author: "John Doe",
  subject: "Chemistry"
};

let Book2: NovelContract = {
  title: "The Gods are not to blame",
  pages: 120,
  author: "Ola Rotimi",
  genre: "Tragedy"
};

Enter fullscreen mode Exit fullscreen mode

Encapsulation

This principle of OOP is concerned with keeping all properties and methods that belongs to an object, inside that object. TypeScript allows one to annotate a code with access modifiers that will determine and control how other objects in our code interact with the properties and methods of an object. This can help when debugging code. It is good practice to declare all properties of a class to be private. This ensures that all use case of that properties lies only within that class itself. This way you are sure that you only need look one place to see all the implementation of the properties of a class.

class Automobiles {
  constructor(protected name: string, private _wheels: number, readonly _type: string) {}

  move() {
    return `moving in it's own way`;
  }
}
Enter fullscreen mode Exit fullscreen mode

Public properties can be accessed and updated from any location in our code and overtime it becomes difficult to keep track of what is changing what and where those values are being changed. a readonly property can only be accessed but not set. Protected properties behave much like private properites. The difference is that a protected property is accessible inside subclasses of a class while private properties are only accessible inside a class.

Delegation

This is an important concept that is concerned with delegating or rather handing over a particular task to another part of your program. The wrapper class calls the delegate class and passes a reference to itself using the this keyword to the delegate class. The delegate class will be able to access properties and methods on the wrapper

interface BookContract {
    title: string
    author: string
}

class Library {
    books: BookContract[] = []
    constructor(private librarian: Librarian){}

    addBook(title, author){
        let book = this.librarian.newBook(title, author)
        this.books.push(book)
    }
}

class Librarian {
    libraries: Library[] = []

    constructor(){
        this.libraries.push(new Library(this))
    }

    newBook(title: string, author: string){
        return { title, author }
    }

    addBooktoLibraries(name, author){
        for(let library of libraries){
            library.addBook(name, author)
        }
    }
}

let superLibrarian = new Librarian()
superLibrarian.addBookToLibraries('The gods are not to blame', 'Ola Rotimi')
Enter fullscreen mode Exit fullscreen mode

In the example above the the librarian class passes a reference to the library class each time a new library is created. Allowing each library to call the newBook method on the librarian each time a new book is added. A library cannot bring a new book but a librarian can so we delegate the task of creating a new book to a library... That is rather than calling newBook outside rather we call newBook inside a library, each library can newBook when required, while a librarian can coordinate library to create and add new books. The librarian is the wrapper while the library is the delegate.

Delegations can help you with abstraction and relationship modeling, there are some cases where a hierarchical relationship is not the best model, you will agree with me that a cat is an animal, and that a cat has whiskers. Delegation can help you express relationships in the form of has a to fit situations where it makes more sense to use than is a. Drawing from our example above we can say that a library has a librarian.

Discussion (0)