Interfaces and Classes in Typescript

FUN PART OF TYPESCRIPT LANGUAGE LIES IN INTERFACES. CLASSES HELP IN MAKING THE PROJECT OBJECT ORIENTED. CLASSES LOGICALLY DIVIDE THE APPLICATION TO HELP US ISOLATE THE CODE BASED ON FUNCTIONALITY

Interfaces

The core principle of Typescript is its type checking. Type checking focuses on the type that the value has and takes many decisions based on type interoperability.

Let us understand with a practical example. Consider the below code.

const object1: {
    size: number,
    type: string,
    shape: string,
    attributes: string,
} = null;

const object2: {
    size: number,
    type: string,
    shape: string,
    attributes: string,
} = {
    size: 10,
    type: 'solid',
    shape: 'circle',
    attributes: 'can roll',
};

The code defines two objects object1 and object2 which share the same type (shape). But this is a lot of duplication as we had to define all the parameters that object1 and object2 posses. What if the object had 100 attributes or what if there were 50 objects that share the same type? This results in a lot of duplication.

Duplication of code is always a bad idea for two main reasons.

  1. They result in a lengthy code
  2. Changes to the code requires editing in multiple places.

This is where interfaces come handy in defining the shape of the object once and reuse them when required.

interface objectShapeImpl {
    size: number,
    type: string,
    shape: string,
    attributes: string,
};

const object1: objectShapeImpl = null; 
const object2: objectShapeImpl = {
    size: 10,
    type: 'solid',
    shape: 'circle',
    attributes: 'can roll',
};

Now we share the type with both the objects and its now easy to make further changes to the code as we just edit the interface object and rest is implied automatically.


Strict Type Checks

Typescript interfaces are strict in nature. The attributes in the interface is mandatory unless we make it optional (which we will see in a while).

interface objectShapeImpl {
    size: number,
    type: string,
    shape: string,
    attributes: string,
};

const object1: objectShapeImpl = null; // This is ok provided there is no strict null check in tsconfig.json

const object2: objectShapeImpl = {
    size: 10,
    type: 'solid',
    shape: 'circle',
    attributes: 'can roll',
}; // This is the best way of doing it

const object3: objectShapeImpl = {
    size: 12,
    type: 'liquid',
    shape: 'shapeless',
}; // This results in a error as attribute is a mandatory field

Optional Attributes

Not all properties of an interface may be required always. The best part of Typescript is its ability to have lenience in defined types. Interfaces can define its one or more attributes to be optional.

interface objectShapeImpl {
    size: number,
    type: string,
    shape: string,
    attributes?: string, // Note ?: which defines the attribute to be optional
};

const object1: objectShapeImpl = null; // This is ok provided there is no strict null check in tsconfig.json

const object2: objectShapeImpl = {
    size: 10,
    type: 'solid',
    shape: 'circle',
    attributes: 'can roll',
}; // This is the best way of doing it

const object3: objectShapeImpl = {
    size: 12,
    type: 'liquid',
    shape: 'shapeless',
}; // No error as attribute is optional

In the above code object3 doesn’t throw an error as attributes is declared optional.


Read-Only Properties

Sometimes we are in need to define an attribute that can be defined once and cannot be modified later.

The object with read-only properties has to be defined when it is created and cannot be modified later.

interface objectShapeImpl {
    size: number,
    type: string,
    readonly shape: string, // Note ?: this is a readonly attribute
    attributes?: string, 
};

const object1: objectShapeImpl = {
    size: 10,
    type: 'solid',
    shape: 'circle',
    attributes: 'can roll',
};

object1.shape = 'square'; // This results in a error ( Cannot assign to 'shape' because it is a read-only property. )

Function Types

Interfaces can define many types. They can not only define Object properties but also other types and one such important type is defining the shape of a function.

This is useful when we wanted to define multiple function with same shape. By shape we are referring to the input arguments and the function return value.

interface MyShapeImpl {
    (shape: string, dimensions: number): void;
}

let shape: MyShapeImpl;

myShape = (shape, dimensions) => {
    console.log(`shape is ${shape} and dimensions are ${dimensions}`)
}

Now myShape is defined the interface MyShapeImpl  We do not define the type for shapeand dimensions when defining myShape function as its predefined in the interface declaration and Typescript is intelligent enough to understand it.


Classes

Javascript was traditionally a functional language with no support for classes. With es6 we now have support for classes but as a programmer from Javascript background will understand, Javascript classes are no more than a special Object definition and are awkward most of the time. Typescript overcomes some of the pain points of Javascript classes.

We define a simple Typescript class as

class Greet {
    word = 'Hello World';

    greet = () => {
        console.log(this.word);
    }
}

const greet = new Greet();

The resulting Javascript code is

"use strict";
class Greet {
    constructor() {
        this.word = 'Hello World';
        this.greet = () => {
            console.log(this.word);
        };
    }
}
const greet = new Greet();

The syntax for Typescript classes are similar to that of Java or other traditional Object oriented programming languages. greet variable is the instance of the class, word is the a variable associated with the class, greet is a function associated with the class and we access the class variables and function with this keyword.


Inheritance

Typescript has the ability to inherit a class into other. The class which inherits is called a child class and the class that is inherited from is  a parent class.

For more details on inheritance refer here.

class Greet {
    word = 'Hello World';

    greet = () => {
        console.log(this.word);
    }
}

class specialGreet extends Greet {
    word = 'John';

    vipGreeting = () => {
        console.log(`Hello Mr.${this.word}`)
    }
}

const greet = new Greet();

The class SpecialGreet inherits all the attributes from Greet along with its own function such as vipGreeting. We do not go in depth here as all the other inheritance features work in almost a similar way.

Note: Typescript doesn’t allow multiple inheritance


Public, Private and Protected

One of the finest feature of Typescript is its ability to restrict access of Class members.

By default all members defined by the class are public unless specified with a special keyword.

Private Members are accessible only inside the class and Protected members are special variables that has restricted access to the parent and child classes with no access to the outside modifiers.

class Greet {
    public method = 'greet';
    private word = 'Hello World';
    protected name = 'John';

    greet = () => {
        console.log(this.word);
    }
}

class specialGreet extends Greet {

    vipGreeting = () => {
        console.log(`Hello Mr.${this.word}`) // Error here as the word is private to the parent class
        console.log(`Hello Mr.${this.name}`) // no error here as child class can access protected members
    }
}

const greet = new Greet();
greet.method = 'main'; // no error here
greet.word = 'text'; // Error here as private members are not accessible outside
greet.name = 'text'; // Error here as protected members are not accessible outside

When the code above is taken to Javascript the access modifiers are completely ignored. These modifiers are only used during development phase and has no effect during run time.

"use strict";
class Greet {
    constructor() {
        this.method = 'greet';
        this.word = 'Hello World';
        this.name = 'John';
        this.greet = () => {
            console.log(this.word);
        };
    }
}
class specialGreet extends Greet {
    constructor() {
        super(...arguments);
        this.vipGreeting = () => {
            console.log(`Hello Mr.${this.word}`); // Error here as the word is private to the parent class
            console.log(`Hello Mr.${this.name}`); // no error here as child class can access protected members
        };
    }
}
const greet = new Greet();
greet.method = 'main'; // no error here
greet.word = 'text'; // Error here as private members are not accessible outside
greet.name = 'text'; // Error here as protected members are not accessible outside

Accessors

The setters and getters are special methods in the class that help us to set or get the private variables from the class which are not accessible from outside of the class.

Typescript has a very handy feature to implement Getters and Setters using get and set keywords.

class Employee {
    private _fullname: string = '';

    get fullName(): string {
        return this._fullname
    }

    set fullName(newName: string) {
        this._fullname = newName
    }
}

The resulting Javascript code has similar structure but completely ignores private and other access modifiers.


Static

So far we have discussed the instance variables and methods which are accessed by creating a new instance of a class.

There are also static methods and classes which are accessed directly with the class names.

class Shape {
    static shape = 'circle';
    static color = 'red';

    static setColor(color: string) {
        this.color = color;
    }
}

const shape = new Shape(); // no error here as its a regular class and can be instantiated.
console.log(shape.shape) // Error here as shape is a static variable and cannot be accessed through instance
console.log(Shape.shape) // No error here
Shape.setColor('orange') // No error here

The Javascript code is

"use strict";
class Shape {
    static setColor(color) {
        this.color = color;
    }
}
Shape.shape = 'circle';
Shape.color = 'red';
const shape = new Shape(); // no error here as its a regular class and can be instantiated.
console.log(shape.shape); // Error here as shape is a static variable and cannot be accessed through instance
console.log(Shape.shape); // No error here
Shape.setColor('orange'); // No error here

Abstract Classes

Abstract classes are base classes from which other classes may be defined. They cannot be instantiated directly but has to be done through the child class which inherits it. Abstract classes generally define the rules of class implementation but do not define the value of them, though there is no error in assigning a value to them.

abstract class Shape {
    abstract edges: number;
    abstract dimensions: number;
    abstract calculateArea: (side: number) => number;
}

class Square extends Shape {
    edges = 4;
    dimensions = 1;
    calculateArea = (side: number) => {
        return side * side
    }
}

class Circle extends Shape {
    edges = 0;
    dimensions = 1;
    calculateArea = (radius: number) => {
        return 3.14 * radius * radius
    }
    getDiameter = (radius: number) => {
        return 2 * radius;
    }
}

let shape: Shape = new Square(); // No error as Square extends Shape.

shape = new Circle(); // No error here as circle also extends Shape.

let square: Square = new Circle(); // No Error as Square has all the met
hods of Circle but a bad programming practise

let circle: Circle = new Square(); // Error Property 'getDiameter' is missing in type 'Square' but required in type 'Circle'

Shape only defines the rules for implementing the class, Square and Circle implements it as defined.

The resulting Javascript code is

"use strict";
class Shape {
}
class Square extends Shape {
    constructor() {
        super(...arguments);
        this.edges = 4;
        this.dimensions = 1;
        this.calculateArea = (side) => {
            return side * side;
        };
    }
}
class Circle extends Shape {
    constructor() {
        super(...arguments);
        this.edges = 0;
        this.dimensions = 1;
        this.calculateArea = (radius) => {
            return 3.14 * radius * radius;
        };
        this.getDiameter = (radius) => {
            return 2 * radius;
        };
    }
}
let shape = new Square(); // No error as Square extends Shape.
shape = new Circle(); // No error here as circle also extends Shape.
let square = new Circle(); // No Error as Square has all the methods of Circle but a bad programming practise
let circle = new Square(); // Error Property 'getDiameter' is missing in type 'Square' but required in type 'Circle'

Using Class as interface

Class can also be used as a interface to represent the shape of the object or functions or another class/

class Point2d {
    x: number;
    y: number;
}

interface Point3d extends Point {
    z: number;
}

let point3d: Point3d = {x: 1, y: 2, z: 3};

Classes and Interface Implementation

This is the core and important concept of Typescript Type implementation and as in other languages classes can implement Interfaces which helps in defining the rules for the class.

In the above example where we used abstract classes to define the shapes, we can implement the same using interfaces

interface Shape {
    edges: number;
    dimensions: number;
    calculateArea: (side: number) => number;
}

class Square implements Shape {
    edges = 4;
    dimensions = 1;
    calculateArea = (side: number) => {
        return side * side
    }
}

class Circle implements Shape {
    edges = 0;
    dimensions = 1;
    calculateArea = (radius: number) => {
        return 3.14 * radius * radius
    }
    getDiameter = (radius: number) => {
        return 2 * radius;
    }
}

let shape: Shape = new Square(); // No error as Square implements Shape.
shape = new Circle(); // No error here as circle also implemented Shape.
let square: Square = new Circle(); // No Error as Square has all the methods of Circle but a bad programming practise
let circle: Circle = new Square(); // ErrorProperty 'getDiameter' is missing in type 'Square' but required in type 'Circle'

This code has same effect as the above one except that the abstract classes are replaced by interfaces and the classes implements them rather than extending them.

Leave a Comment