TypeScript Generic Function, Class and Interface (+Examples)

In TypeScript, generic interfaces, classes, and methods enable developers to write more flexible and reusable code using the type parameters. These type parameters are used as types in the construct to represent some type that can be different in each instance of the construct. It improves the code reusability and type safety.

In this TypeScript tutorial, we will learn about generic interfaces, classes, and methods and explore their syntax, usage, and benefits.

1. Generic Functions

A function may be made generic by placing an alias for a type parameter, wrapped in angle brackets, immediately before the parameters parentheses. This type parameter will then be available for usage in parameter type annotations, return type annotations, and type annotations inside the function’s body.

1.1. Syntax

The syntax to define a generic function is:

function functionName<T>(param1: T, param2: T): T {
    // Function body
}
  • ‘<T>’: Specifies the type parameter.
  • ‘param1’, ‘param2’: Parameters of type T.
  • ‘: T’: Specifies the return type.

1.2. Generic Function Example

In the following example, we have an add() function that can accept either string or number-type parameters. Based on the type of parameters, the function either appends the strings or adds the numbers.

function add<X, Y>(x: X, y: Y): string|number  {

    if (typeof x === 'string' && typeof y === 'string') {
        return x + y;
    } else if (typeof x === 'number' && typeof y === 'number') {
        return x + y;
    } else {
        throw new Error('Invalid types. Expected two strings or two numbers.');
    }
}

// Example usage
const result1: string = add('Hello', ' world');
console.log(result1); // Output: Hello world

const result2: number = add(5, 3);
console.log(result2); // Output: 8

Thus, adding type parameters to functions in this way allows them to be reused with different inputs while still maintaining type safety and avoiding any types.

Try not to use more than one or two type parameters in any generic construct. As with runtime function parameters, the more you use, the harder it is to read and understand the code.

2. Generic Classes

Similar to generic functions, generic classes can also declare any number of type parameters to be later used on members. Each instance of the class may have a different set of type arguments for its type parameters.

2.1. Syntax

The syntax to define a generic class is:

class ClassName<T> {
  private property1: T;
  private property2: T;

  constructor(param1: T, param2: T) {
    // Constructor body
  }

  methodName(param: T): T {
   // Method body
  }
}
  • ‘<T>’: Specifies the type parameter.
  • ‘property1’, ‘property2’: Properties of type T.
  • ‘param1’, ‘param2’: Constructor parameters of type T.
  • ‘methodName’: Method with parameter and return type T.

2.2. Generic Class Example

In the following example, Pair is a generic class that allows to store pair of values of a similar type only.

class Pair<T> {

  private first: T;
  private second: T;

  constructor(first: T, second: T) {
    this.first = first;
    this.second = second;
  }

  getFirst(): T {
    return this.first;
  }

  setFirst(value: T): void {
    this.first = value;
  }

  getSecond(): T {
    return this.second;
  }

  setSecond(value: T): void {
    this.second = value;
  }
}

Now we can add only similar types of values in a pair. See the following example which stores the values of number in pair1, and type string in pair2.

const pair1: Pair<number> = new Pair<number>(3, 5);
console.log(pair1.getFirst()); // Output: 8

const pair2: Pair<string> = new Pair<string>('Hello', 'World');
console.log(pair2.getFirst()); // Output: Hello

3. Generic Interfaces

Similar to generic classes, interfaces may be declared as generic as well. The rule remains the same. A generic interface declaration may have any number of type parameters declared between a ‘<‘ and ‘>’ after their name. That generic type may later be used elsewhere in their declaration, such as on member types.

3.1. Syntax

The syntax to define a generic interface is:

interface InterfaceName<T> {
  property1: T;
  property2: T;

  methodName(param: T): T;
}
  • ‘<T>’: Specifies the type parameter.
  • ‘property1’, ‘property2’: Properties of type T.
  • ‘methodName’: Method with parameter and return type T.

3.2. Generic Interface Example

For example, the built-in Array methods are defined in TypeScript as a generic interface. The array uses a type parameter T to represent the type of data stored within an array. Its pop and push methods look roughly like so:

interface Array<T> {
  
  pop(): T | undefined;
  push(...items: T[]): number;

  // ...
}

Similarly, we can define our generic interfaces, LinkedNode and LinkedList as follows. This example declares two generic interfaces that may contain a value of a specific type only.

interface LinkedNode<T> {

  value: T;
  next: LinkedNode<T> | null;
}

// Define a generic interface for a linked list
interface LinkedList<T> {

  head: LinkedNode<T> | null;
  length: number;

  append(value: T): void;
  remove(value: T): void;
  toArray(): T[];
}

Further, we can implement an instance of these interfaces as follows:

// Implement a concrete class for the generic linked list
class ConcreteLinkedList<T> implements LinkedList<T> {

  head: LinkedNode<T> | null = null;
  length: number = 0;

  append(value: T): void {
    const newNode: LinkedNode<T> = { value, next: null };
    if (!this.head) {
      this.head = newNode;
    } else {
      let current: LinkedNode<T> | null = this.head;
      while (current.next) {
        current = current.next;
      }
      current.next = newNode;
    }
    this.length++;
  }

  remove(value: T): void {
    if (!this.head) {
      return;
    }
    if (this.head.value === value) {
      this.head = this.head.next;
      this.length--;
      return;
    }
    let current: LinkedNode<T> | null = this.head;
    while (current.next) {
      if (current.next.value === value) {
        current.next = current.next.next;
        this.length--;
        return;
      }
      current = current.next;
    }
  }

  toArray(): T[] {
    const result: T[] = [];
    let current: LinkedNode<T> | null = this.head;
    while (current) {
      result.push(current.value);
      current = current.next;
    }
    return result;
  }
}

Finally, we can use the ConcreteLinkedList class to add and remove elements of a certain type only, thus getting the type safety.

const linkedList: LinkedList<number> = new ConcreteLinkedList<number>();

linkedList.append(1);
linkedList.append(2);
linkedList.append(3);
console.log(linkedList.toArray()); // Output: [1, 2, 3]

// linkedList.append("four");
// Argument of type 'string' is not assignable to parameter of type 'number'.

4. Conclusion

This typescript tutorial discussed how to define generic functions, generic classes, and generic methods with examples. Remember that we should limit the number of generic parameters in a construct to keep the code clean and readable.

Happy Learning !!

Comments

Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.