Object Oriented Programming using Typescript: A Complete Guide

Object Oriented Programming using Typescript: A Complete Guide

Classes

They are the blueprint for its instances (objects that the class produce).

JavaScript has had classes since the ES5 language implementations. In there it was introduced to us the native implementations of “classes” was. Typescript extends the functionalities of that feature to make it look more like more common programming languages like Java for example.

Before we continue, if you're in a hurry, here's the GitHub repository with all the code of this guide, just in case.

Now, let’s see what functionalities are and how to use them.

Basic declaration

There is no mystery here. You use the keyword class, next to the name of the class (starting it with Uppercase by convention) and open brackets.

/* ------------- Basic declaration ------------ */
class A {
    a1: string;
    a2: number;
    a3: boolean;

    constructor(a1: string, a2: number, a3: boolean) {
        this.a1 = a1;
        this.a2 = a2;
        this.a3 = a3;
    }
}

In that snippet of code, you have a1 , a2 and a3 declared at the top of the class, that’s because Typescript will use that to tell this that those are part of its constructor and will not throw an error nor syntax warnings on the editor.

I know that’s a little too much boilerplate for just a simple class, but don’t worry, keep reading and I’ll show you a shorter way of doing that.

Then you can instantiate it like this:

const aa = new A("New Class", 1, true);
console.log("Basic declaration:", aa);

With methods

Went you wanna add methods, you don’t need to declare it at the top of the class nor do you have to use the keyword function.

/* --------------- With methods --------------- */
class B {
    b1: string;
    b2: number;
    b3: boolean;

    constructor(b1: string, b2: number, b3: boolean) {
        this.b1 = b1;
        this.b2 = b2;
        this.b3 = b3;
    }

    run(time: "all day" | "just the mornings" | "anytime"): void {
        console.log(`I'm running ${time}, so you have this: ${this.b1} times ${this.b2} and it's ${this.b3}`);
    }
}
const bb = new B("Power", 59, true);
console.log("B class:", bb);
bb.run("all day");

Encapsulation

Sometimes you’ll want to limit the kind of information that the instances share with the rest of the code, in those cases, you’ll use encapsulation.

The default way that classes instantiate their properties is public. Anyone can see them, anyone can change them, and anyone can manipulate that data.

Then, you have private properties which can only be manipulated by the class itself. By convention, you start the name of a private with an underscore _ and they are strongly related to getters and setters, which I’ll explain in a moment

Some properties can only be read and cannot be modified, not even by the class itself, those are readonly .

And finally, we have protected which is very similar to private but with the difference that protected properties can also be modified by inherited classes (I’ll talk a little bit about that later).

/* --------------- Encapsulation -------------- */
class C {
    c0: any // public by default
    public c1: null | number;
    private _c2: boolean | string;
    readonly c3: string;
    protected _c4: string;

    constructor({ c1, c2, c3, c4 }: { c1: null | number, c2: boolean, c3: string, c4: string, }) {
        this.c0 = "Whatever";
        this.c1 = c1;
        this._c2 = c2;
        this.c3 = c3;
        this._c4 = c4;
    }

    get c2() {
        return `Mr. C2 says that  it's value is: ${this._c2}. Now you can go, please`;
    }
    set c2(newValue: boolean | string) {
        // this.c3 = "Nope, still not changing"; // it will change the value, but the idea is to listen to typescript
        this._c2 = newValue;
    }

    set c4(newValue: string) {
        this._c4 = newValue;
    }

}
const cc = new C({ c1: null, c2: false, c3: "You can only see me, not modify me", c4: "Only the class can make me change and it's children" });
console.log("Encapsulation with class:", cc);

cc.c0 = "I'm public, whatever";
cc.c1 = 975;
// cc._c2 = true; // it will change the value, but the idea is to listen to typescript
cc.c2 = "VIP";
// cc.c3 = "Time to change... hahaha, not really"; // it will change the value, but the idea is to listen to typescript
// cc._c4 = "I told you, only the class can change me, not you"; // it will change the value, but the idea is to listen to typescript
cc.c4 = "Oh, well, if you say it please.... okey, let's go";

console.log("After changes:", cc)
console.log(cc.c2)
console.log(cc.c3)
// console.log(cc._c4) // it will display the value, but the idea is to listen to typescript

In the code above you’ll see a lot of comments, that’s because since this is an implementation of Typescript, many of these keywords don’t exist natively on Javascript, so the changes that you make in those scenarios are, even if they are invalid to Typescript, it will still run went it transpile to Javascript.

Getters and Setters

Although this is not specific to Typescript but also Javascript. Getters and Setters are used to set or get a value of a property.

They are useful when you are using private or protected properties but you still want a way for the class to expose those values without directly referencing them.

Using the example above, they would like something like this:

class C {
    private _c2: boolean | string;

    constructor({ c2, }: { c2: boolean }) {
        this._c2 = c2;
    }

    get c2() {
        return `Mr. C2 says that  it's value is: ${this._c2}. Now you can go, please`;
    }
    set c2(newValue: boolean | string) {
        // this.c3 = "Nope, still not changing"; // it will change the value, but the idea is to listen to typescript
        this._c2 = newValue;
    }

}

Get

get will return the value of the property.

console.log(cc.c2)

Set

set can overwrite the value of the private property.

cc.c2 = "VIP";

Short constructor

To avoid all the boilerplate to declare the variables with their types at the beginning of the class, you can use attributes of the constructor to short that off. The only condition is to add the access type of each variable.

/* ------------- Short constructor ------------ */
class D {
    constructor(
        // d0: any // you need to add the type of access: public, private, etc
        public d1: number,
        private d2: boolean,
        readonly d3: string,
        protected d4: number = 888,
    ) { }

    d5(): void {
        console.log("Just logging");
    }
}
const dd = new D(13, true, "Reading...", /* 99 */);
console.log("Short constructor", dd);

Inheritance

A class can have all the methods and properties that another class has. To do so, one class extends the functionalities of another, this process is called inheritance because, in some sense, the class extended will become a “parent” and the other a “child” that will inherit everything that its “parents” has.

As a side note, a class can only inherit one class at a time.

/* ---------------- Inheritance --------------- */
class E {
    constructor(
        public e1: number,
        private e2: string = "Hi, I'm VIP",
        protected e3: string = "Hello, I'm can be used be the inheritance",
    ) { }

    e4() {
        console.log("%c Here, just doing some Class E stuff", "color: pink; background-color: black",);
    }
}
class F extends E {
    constructor(
        e1: number, // for the super class don't put the access type (public, private, etc)
        public f1: number,
    ) {
        super(e1);
    }

    f2() {
        console.log(`Super thing 1 ${this.e1}`);
        // console.log(`Super thing 2 ${this.e2}`); // privates can be extended to the children
        console.log(`Super thing 3 ${this.e3}`);

        super.e4();
    }
}

const ee = new E(2000);
const ff = new F(2501, 3000);
console.log(ee);
console.log(ff);
ff.f2();
// class G extends A, B, C, D, {} // it can only extend 1 class at the time

Static methods

With the static keyword you can use properties and methods (mostly methods) without instantiating the class but with the class itself. Just keep in mind that the object instance this will not work as expected since there will be nothing instantiated yet.

/* ------------------ Statics ----------------- */
class G {
    constructor(
        public gg = "Good game, gg",
    ) { }

    static g = "gg, GG, gg";
    static ggGGgg() {
        console.log(`By the way, gg. ${this.gg}. Woops, I forgot that you need to initialize my first, unless... ${this.g}...`);
    }
}
G.ggGGgg();
console.log(G.g)

Abstract Classes

An abstract class is the class you don’t want to instantiate because they are too open in its definition and too vague to make an object out of it. Instead, what you wanna do is to define it and let other classes inherit from it. That’s the whole point of an abstract class.

/* ----------------- Abstract ----------------- */
// when a class is too general
// and you don't want to allow instances of that class
// but you still want to inherit
// then you can use an abstract class
interface ILiving {
    name: string;
    exists(): void;
}
abstract class LivingThing implements ILiving { // ignore the "interface" and "implements" for now
    constructor(
        public name: string = "perrito",
    ) { }

    exists(): void {
        console.log("Existing");
    }
}

// const livingThing = new LivingThing("Creature"); // Just don't, ok?
class Dog extends LivingThing {
    constructor(
        name: string,
        public power: string,
    ) {
        super(name);
    }

    move(): void {
        console.log("Moving using 4 legs");
    }
}
const dog = new Dog("Perrito", "Perfection");
console.log("Extending from abstract class", dog);
dog.exists();
dog.move();

Interfaces in classes

Interfaces are related to objects and only exist on Typescript, not in Javascript. They declare what properties and methods the object will have and what type of data it’ll manage.

interface H {
    h1: number;
    h2(): number;
}

Like classes, they can extend from each other. But, there is no such thing as polymorphism, which I’ll explain in another moment, but means that you can’t overwrite a property, for example h1 with another interface.

/* ---------------- Interfaces ---------------- */
interface H {
    h1: number;
    h2(): number;
}
interface I {
    i1: string;
    i2: ILiving;
}
interface J extends H, I {
    j1: boolean;
}

And you use the keyword implements to use them in your classes.

class K implements H, I, J { // in this example implementing "H" and "I" are optionals since "J" already does that
    constructor(
        public h1: number,

        public i1: string,
        public i2: ILiving,

        public j1: boolean,
    ) {}

    h2() {
        return 66;
    }
}
const kk = new K(44, "Welcome", dog, true);
console.log("Interfaces and classes =>", kk)

Well, there you have it. A complete guide to Object Oriented Programming using Typescript. If you have anything to add to it please comment on it or reach out to me through my social media:

Twitter, LinkedIn, Instagram, Github, Medium (For more opinion-based posts), and here on Hashnode and my Hashnode blog. Also, follow me if you want XD

Remember that here's the repository with all the code.

Have a nice day and until next time. Bye.