TypeScript - Generic Classes



Generic Classes

TypeScript Generic classes allow you to create a class that can work over a variety of data types rather than a single one. It increases the scalability and reusability of the code. Let's understand how generic classes work in TypeScript.

Syntax

You can follow the syntax below to use the generic classes in TypeScript.

class class_name<T, U> {
    // Class body
}
let obj1 = new class_name<data_type_1, data_type_2>();
  • In the above syntax, 'class' is a keyword to define a class.

  • 'class_name' is a valid identifier, representing the class name.

  • '<T, U>' are type parameters specified in the angular bracket. You can specify as many as you want.

  • While defining the object of the class, you need to pass data types as an argument after the class name in the angular bracket.

Example

In the code below, we have defined the generic class named 'Box' which takes type parameter T.

In the class, we have defined the 'val' variable of type T, and the constructor function which initializes the value of the 'val' variable.

After that, we defined the getters and setters named get() and set(), respectively to get the value of the 'val' variable.

Next, we have defined the 'box1' and 'box2' objects of the Box class which take number and string data type as a type parameter argument, respectively.

// generic class
class Box<T> {
    // member variable
    val: T;

    // constructor with value
    constructor(value: T) {
        this.val = value;
    }

    // Method to get value
    get(): T {
        return this.val;
    }

    // Method to set value
    set(value: T): void {
        this.val = value;
    }
}

// create object of Box class
let box1 = new Box<number>(10);
console.log(box1.get()); // 10

let box2 = new Box<string>("Hello");
console.log(box2.get()); // Hello

On compiling, it will generate the following JavaScript code:

// generic class
class Box {
    // constructor with value
    constructor(value) {
        this.val = value;
    }
    // Method to get value
    get() {
        return this.val;
    }
    // Method to set value
    set(value) {
        this.val = value;
    }
}
// create object of Box class
let box1 = new Box(10);
console.log(box1.get()); // 10
let box2 = new Box("Hello");
console.log(box2.get()); // Hello

Output

The output of the above code is as follows –

10
Hello

Example

In the TypeScript code below:

  • We have defined the 'Stack' class which takes a type parameter 'T'.

  • In the class, we have defined the private variable 'st' whose type is an array of type T.

  • The constructor function initializes the 'st' array.

  • Push() method takes the element of type 'T' as a parameter and inserts it in the 'st' array.

  • The pop() method removes the last element from the 'st' array and returns it.

  • The peek() method returns the last element from the array.

  • The isEmpty() method returns a boolean value based on whether the array is empty.

  • The size() method returns the size of the 'st' array.

  • Next, we have defined the object of the Stack class with the number data type, performed various operations using the methods of the Stack class.

// Defining the class stack
class Stack<T> {
    // Defining the private array to store the stack elements
    private st: T[] = [];

    // Constructor to initialize the stack with initial contents
    constructor(initialContents?: T[]) {
        if (initialContents) {
            this.st = initialContents;
        }
    }

    // Method to push an element to the stack
    push(item: T): void {
        this.st.push(item);
    }

    // Method to pop an element from the stack
    pop(): T | undefined {
        return this.st.pop();
    }

    // Method to get the top element of the stack
    peek(): T | undefined {
        return this.st[this.st.length - 1];
    }

    // Method to check if the stack is empty
    isEmpty(): boolean {
        return this.st.length === 0;
    }

    // Method to get the size of the stack
    size(): number {
        return this.st.length;
    }
}

// Usage Example
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);

console.log(numberStack.peek());  // Outputs: 3
console.log(numberStack.pop());   // Outputs: 3
console.log(numberStack.peek());  // Outputs: 2
console.log(numberStack.isEmpty()); // Outputs: false
console.log(numberStack.size());    // Outputs: 2

On compiling, it will generate the following JavaScript code:

// Defining the class stack
class Stack {
    // Constructor to initialize the stack with initial contents
    constructor(initialContents) {
        // Defining the private array to store the stack elements
        this.st = [];
        if (initialContents) {
            this.st = initialContents;
        }
    }
    // Method to push an element to the stack
    push(item) {
        this.st.push(item);
    }
    // Method to pop an element from the stack
    pop() {
        return this.st.pop();
    }
    // Method to get the top element of the stack
    peek() {
        return this.st[this.st.length - 1];
    }
    // Method to check if the stack is empty
    isEmpty() {
        return this.st.length === 0;
    }
    // Method to get the size of the stack
    size() {
        return this.st.length;
    }
}
// Usage Example
const numberStack = new Stack();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.peek()); // Outputs: 3
console.log(numberStack.pop()); // Outputs: 3
console.log(numberStack.peek()); // Outputs: 2
console.log(numberStack.isEmpty()); // Outputs: false
console.log(numberStack.size()); // Outputs: 2

Output

The output of the above code is as follows –

3
3
2
false
2

Implementing Generic Interface with Generic Classes

Generic classes can also implement the generic interfaces. So, developers can use a single generic interface to implement multiple generic classes, allowing them to reuse the code.

Syntax

You can follow the syntax below to implement the generic interface with generic classes.

class class_name<T> implements interface_name<T> {
    // Class body
}
  • In the above syntax, 'class class_name<T>' defines the generic class.

  • 'implements' is a keyword to implement the interface with the class.

  • 'interface_name<T>' is a generic interface.

Example

In the example below:

  • We have defined the generic interface named 'dataBase', which defines the findById() and save() method.

  • Next, we have defined the generic class named 'memorydataBase', and implemented it with the 'dataBase' interface.

  • In the class, we have defined the 'items' map which stores the numeric value as a key, the value of type 'T'.

  • Next, we have implemented the findById() method, which accesses the value by key from the map and returns it.

  • The save() method stores the key-value pair in the 'items' map.

  • In the end, we created the object of the 'MemorydataBase' class and performed various operations using this method.

// Defining a generic interface
interface dataBase<T> {
    findById(id: number): T | undefined;
    save(item: T): void;
}

// Defining a class that implements the generic interface
class MemorydataBase<T> implements dataBase<T> {
    // Defining a private property that is a map of items
    private items = new Map<number, T>();

    // Implementing the findById method
    findById(id: number): T | undefined {
        return this.items.get(id);
    }

    // Implementing the save method
    save(item: T): void {
        const id = this.items.size + 1;
        this.items.set(id, item);
    }
}

// Creating an instance of the MemorydataBase class
const repo = new MemorydataBase<string>();
repo.save("Hello");
console.log(repo.findById(1)); // Outputs: Hello

On compiling, it will generate the following JavaScript code:

// Defining a class that implements the generic interface
class MemorydataBase {
    constructor() {
        // Defining a private property that is a map of items
        this.items = new Map();
    }
    // Implementing the findById method
    findById(id) {
        return this.items.get(id);
    }
    // Implementing the save method
    save(item) {
        const id = this.items.size + 1;
        this.items.set(id, item);
    }
}
// Creating an instance of the MemorydataBase class
const repo = new MemorydataBase();
repo.save("Hello");
console.log(repo.findById(1)); // Outputs: Hello

Output

The output of the above code is as follows –

Hello

You may use the 'extends' keyword to use the various constraints with the generic classes. It's always a good idea to use generic parameters, constraints, interfaces, and classes in your code to make it scalable and reusable.

Advertisements