[Typescript OOP 1/3] Starting with classes

[Typescript OOP 1/3] Starting with classes

This is the 1st of a 3-part series where I explain working with Object Oriented architecture on Typescript

To explan classes on typescript, I think it is imperative that I begin with what is typescript!

What is Typescript?

To put it bluntly,

Typescript is Microsoft's attempt at bringing order and discipline in the most talented yet indisciplined of all programming languages - Javascript.

However, underneath it is all Javascript. Typescript is Javascript with type restrictions which help in development time and compile time improvements with error reductions. Typescript is then transpiled into Javascript by pruning out all its type restrictions from the code and removing any type checks that we might put in place like interfaces.

Having said that, let's begin with our topic:

Classes

Creating classes in TS is fairly easy. An identifier keyword class is all that we need.

class Person {}

We created our first class. 🥳

As you can see this is a barebones class. It does nothing. But it is definitely error-free.

Let's add a few properties to it and a couple of methods to our class.

class Person
{
  /** age of the person */
  private age: number = 25;  // private age = 25; also infers type number to 'age' automatically
  /** name of the person */
  private name: string = "FooBar";  // private name = "FooBar" also infers type string to 'name' automatically


  /** greet message for the person */
  private getGreetMessage(): string
  {
    return `Hello world! My name is ${this.name} and I am ${this.age} years old.`
  }
  /** method to greet others */
  greet(): void
  {
    console.log(this.getGreetMessage())
  }
}

So, we declared a class Person with properties age and name which are private. It has a private method getGreetMessage that creates a string combining a hello world message with its name and age. And it has a public method greet that logs the greet message to console.

A few follow-ups from what we just did in the above example.

  1. private methods/functions and variables are accessible only from within the class.
  2. 'public' methods and variables can be declared without any access modifier keywords.
  3. Variables that are being declared and initialized at the same time with primitive types like number, string, and boolean do not need to have their type explicitly entered.

Classes are like wrappers consisting of properties and methods that can be instantiated. What that means is we can have an object in our code that contains all the properties and behaviors described in these classes.

let foo = new Person();
// foo has only one method now => greet()
// foo.age, foo.name and foo.getGreetMessage() do not exist on its *instance*.

foo.greet();
// Hello world! My name is FooBar and I am 25 years old.

This class is quite specific to only one person "FooBar" of a fixed age. That is not so useful for us. Let's bring out the big gun => constructor. We can rewrite the same thing like below and make it more dynamic and adaptable to different scenarios.

class Person
{
  /** age of the person */
  private age: number;  // Declaration only. Initialized in the constructor. Hence, type is required now.
  /** name of the person */
  private name: string;  // Declaration only. Initialized in the constructor. Hence, type is required now.
  /** Prevent any modifications boolean */
  isReadonly = false;


  /** Constructor: Initialization of properties */
  constructor(name: string, age: number)
  {
    this.age = age;
    this.name = name;
  }


  /** greet message for the person */
  private getGreetMessage(): string
  {
    return `Hello world! My name is ${this.name} and I am ${this.age} years old.`
  }
  /** method to greet others */
  greet(): void
  {
    console.log(this.getGreetMessage())
  }
  /** sets age */
  setAge(newAge: number): void
  {
    if (this.isReadonly)
    {
      console.warn("Attempt to set age when it is set to readonly")
      return;
    }
    this.age = newAge;
  }
}

Fun fact: when this code is transpiled to Javascript, it loses all its access controls and declarations without initializations. So, at runtime, any calls directly to any of its former private members and methods should be allowed. However, since we create JS files from our TS files, our code will not suddenly change to include those calls. But just to let you know, these restrictions exist only on compile/development time.

This is how the JS code will look like for our TS class declaration above.

"use strict";
class Person {
    /** Constructor: Initialization of properties */
    constructor(name, age) {
        /** Prevent any modifications boolean */
        this.isReadonly = false;
        this.age = age;
        this.name = name;
    }
    /** greet message for the person */
    getGreetMessage() {
        return `Hello world! My name is ${this.name} and I am ${this.age} years old.`;
    }
    /** method to greet others */
    greet() {
        console.log(this.getGreetMessage());
    }
    /** sets age */
    setAge(newAge) {
        if (this.isReadonly) {
            console.warn("Attempt to set age when it is set to readonly");
            return;
        }
        this.age = newAge;
    }
}

Notice how all the access controls and type outputs like string and void are now gone. This is plain Javascript now. It can work on all browsers directly. To get your Typescript code converted to Javascript code on the fly, visit Typescript Playground.

Coming back to our Person class declaration,

What is the behavior of the Person class now? This class is quite similar to the previous Person class with the following distinctions:

  1. Age and name are not initialized in the beginning. They are set individually for each instance of this class.
  2. A public boolean member isReadonly is declared and initialized with false.
  3. A setAge method that sets a new age for the object if it is not readonly.

Let's look at an instantiation example for our class declaration on TS.

let ram = new Person("Alluri Sitarama Raju", 37);
let bheem = new Person("Komaram Bheem", 38);

// after some processing, it is time to update bheem's age to 39.
bheem.setAge(39);  // done!

// FYI: bheem.setAge("foobar") will result in a compiler error as setAge can only receive number argument

// let's seal age for both our guys
// any further attempt to .setAge for ram and bheem will leave their ages unchanged
ram.isReadonly = true;
bheem.isReadonly = true;

ram.setAge(38);
// A warning will appear on console -> Attempt to set age when it is set to readonly

ram.greet();
// Hello world! My name is Alluri Sitarama Raju and I am 37 years old.
bheem.greet();
// Hello world! My name is Komaram Bheem and I am 39 years old.

So, this is how we can declare classes with access-controlled properties and methods along with type checking to ensure we have some order and discipline in the house to minimize errors on runtime as much as possible. This is important because it brings a compilation phase to our code releases as Javascript is never compiled and it loves to bring runtime error surprises.

In the next post, we will learn more about "this" keyword that we used in our class keyword. We will also learn about scopes and the binding of this keyword. We will also look at how arrow functions are different from normal function declarations from the scope point of view.

Thank You!