TypeScript Fundamentals - Classes

Github Gist

The core strength of TypeScript is the ability it gives developers to use modern syntax in building their JavaScript programs while still maintaining cross browser coverage. The above statement manifests itself with classes in TypeScript where we can use the familiar class syntax from traditional programming languages like C++ and Python to write our code and have TypeScript compile it down to classes and prototype-based inheritance solutions. We will look at how to write classes in TypeScript in a way that is familiar to most programmers coming from another language.

Let’s start by looking at the definition of a class in TypeScript. I will show the code first then explain how the code is setup. We are creating a Matrix class that takes a 2x2 matrix and sets its values based on an anonymous function that is passed when defining the matrix.

interface IMatrix {
  width: number;
  height: number;
  content: string[];
  dimensions: string;
  getElement(x: number, y: number): string;
  setElement(x: number, y: number, value: string): void;
  printMatrix(): string;
}

interface SetMatrixFunc {
  (x: number, y: number): string;
}

interface CMatrix {
  new (width: number, height: number, element: SetMatrixFunc): IMatrix;
}

const Matrix: CMatrix = class Matrix implements IMatrix {
  width: number;
  height: number;
  content: string[] = [];
  private setterFunction: (x: number, y: number) => string;

  constructor(w: number, h: number, el: (x: number, y: number) => string) {
    this.width = w;
    this.height = h;
    this.setterFunction = el;
    this.initializeMatrix(this.setterFunction);
  }

  private initializeMatrix(el: (x: number, y: number) => string): void {
    for (let y = 0; y < this.height; y++) {
      for (let x = 0; x < this.width; x++) {
        this.content[y * this.width + x] = el(x, y);
      }
    }
  }

  get dimensions(): string {
    return `${this.width} x ${this.height}`;
  }

  set dimensions(str: string) {
    //The string passed will contain the dimension in this format: 'x-y'
    let [x, y] = str.split('-');
    this.width = parseInt(x, 10);
    this.height = parseInt(y, 10);
    this.initializeMatrix(this.setterFunction);
  }

  getElement(x: number, y: number): string {
    return this.content[y * this.width + x];
  }

  setElement(x: number, y: number, value: string): void {
    this.content[y * this.width + x] = value;
  }

  printMatrix(): string {
    let row = '';
    for (let x = 0; x < this.width; x++) {
      for (let y = 0; y < this.height; y++) {
        row += ` ${this.getElement(x, y)}`;
      }
      row += '\n';
    }
    return row;
  }
};

let matrix = new Matrix(3, 2, (x, y) => `${x}${y}`);

console.log(matrix.printMatrix()); // 00 01 10 11 20 21
console.log(matrix.dimensions); // 3 x 2

matrix.dimensions = '3-3';

console.log(matrix.width); // 3
console.log(matrix.dimensions); // 3 x 3
console.log(matrix.printMatrix()); // 00 01 02 10 11 12 20 21 22

IMatrix is the type for the instance side of the class and CMatrix is the type for the static side of the matrix. We then define a constructor that sets the properties (width, heigh, content) of the class.

The member setterFunction is set as private since it holds the function used for initializing the matrix and the details of it does not need to be exposed outside of the class. We also set the initializeMatrix function to a private function to hide its details from outside the class.

With TypeScript, we can create getters and setters as a way of intercepting accesses to a member of an object. This allows us to control the way our members are being used. In the above code we have a setter and getter called dimensions which will be used to get the dimension of the array. When we change the dimension through the setter, we also need to recreate the content property with the new dimensions.

A setter and getter for the same member attribute has to be the same type. Since the getter for dimension returns a string, the setter needs to take a string. This is why we pass a string and parse it to get the dimensions. We could do further checks to make sure the format of the string is correct before proceeding to parse the string.

Inheritance

A symmetric matrix is one where the value at x,y is always the same as that at y,x. We can create a symmetric matrix class but because all the functions we defined above will apply to our class, we need the new class to be a subclass of the Matrix class, which will allow it to inherit all the properties of its parent. This can easily be done with TypeScript as well.

interface CSymMatrix {
  new (size: number, el: SetMatrixFunc): IMatrix;
}

const SymmetricMatrix: CSymMatrix = class SymmetricMatrix
  extends Matrix
  implements IMatrix
{
  constructor(size: number, el: (x: number, y: number) => string) {
    super(size, size, (x, y) => {
      if (x < y) return el(y, x);
      else return el(x, y);
    });
  }

  setElement(x: number, y: number, value: string): void {
    super.setElement(x, y, value);
    if (x < y) {
      super.setElement(y, x, value);
    }
  }
};

let sMatrix = new SymmetricMatrix(3, (x, y) => `${x}${y}`);
console.log(sMatrix.printMatrix()); // 00 10 20 10 11 21 20 21 22

SymmetricMatrix extends Matrix and inherits all the properties and members of the parent class. We must call super in the constructor to setup the instance with the parent’s constructor as well. In our case since we only have size as the dimension, we have to pass it twice to the parent constructor for both width and height.

Statics

TypeScript classes support static properties that are shared by all the instances of the class. We put and access static properties on the class itself. Taking the example above, we can add a static member called instances that tracks the number of objects instantiated from the SymmetricMatrix class. Since this member is put on the class side, we also have to add it to the CSymMatrix interface.

interface CSymMatrix {
  new (size: number, el: SetMatrixFunc): IMatrix;
  instances: number;
}

const SymmetricMatrix: CSymMatrix = class SymmetricMatrix
  extends Matrix
  implements IMatrix
{
  static instances = 0;

  constructor(size: number, el: (x: number, y: number) => string) {
    super(size, size, (x, y) => {
      if (x < y) return el(y, x);
      else return el(x, y);
    });
    SymmetricMatrix.instances++;
  }

  setElement(x: number, y: number, value: string): void {
    super.setElement(x, y, value);
    if (x < y) {
      super.setElement(y, x, value);
    }
  }
};

let sMatrix = new SymmetricMatrix(3, (x, y) => `${x}${y}`);
console.log(sMatrix.printMatrix()); // 00 10 20 10 11 21 20 21 22

let bMatrix = new SymmetricMatrix(2, (x, y) => `${x}${y}`);
console.log(bMatrix.printMatrix()); // 00 10 10 11

console.log(SymmetricMatrix.instances); // 2

Instantiating two objects from the SymmetricMatrix class sets the instances static property to the number of times we called the constructor and we can see that logging the property gives us 2.