Using Generic with Class in TypeScript: A Complete Guide

Updated: January 8, 2024 By: Guest Contributor Post a comment

Introduction

Generics in TypeScript enhance code flexibility and reusability by allowing classes, interfaces, and functions to operate with a variety of types. This tutorial will delve into creating and using generic classes, helping you harness the power of type-safe code.

Understanding Generics

To begin with generics, let’s understand the problem they solve. Without generics, you might define a function that accepts any type, losing the type information:

function identity(arg: any): any {
  return arg;
}

However, a generic function retains this information by defining one or more type variables:

function identity<T>(arg: T): T {
  return arg;
}

Here, T serves as a placeholder for any type, passed when the function is invoked.

Generic Classes

A generic class follows a similar concept, where the class defines a blueprint for objects that operate upon a specific but unknown type until the class is instantiated:

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

This class could then be used with numeric types, for instance:

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

The same class can be utilized with strings too:

let stringNumeric = new GenericNumber<string>();
stringNumeric.zeroValue = '';
stringNumeric.add = function(x, y) { return x + y; };

Working with Multiple Type Variables

Generics can have multiple type variables to encapsulate interactions between variables of different types:

class KeyValuePair<K, V> {
  private key: K;
  private value: V;

  public setKeyValue(key: K, value: V): void {
    this.key = key;
    this.value = value;
  }

  public display(): void {
    console.log(`Key = ${this.key}, Value = ${this.value}`);
  }
}

Here’s how you’d instantiate it.

let pair = new KeyValuePair<number, string>();
pair.setKeyValue(1, 'One');
pair.display();

Extending Generic Classes

You might also extend these classes to create new ones, imbued with additional functionality:

class BaseCollection<T> {
  private items: T[] = [];

  public addItem(item: T): void {
    this.items.push(item);
  }

  public getItem(index: number): T {
    return this.items[index];
  }
}

class Stack<T> extends BaseCollection<T> {
  public popItem(): T {
    return this.items.pop();
  }
}

Here, Stack<T> gains the ability to operate upon a collection of any type.

Generic Constraints

Constraints allow you to limit the kinds of types that can be used in generics, enforcing attributes or capabilities:

function merge<T extends object, U extends object>(first: T, second: U): T & U {
  return {...first, ...second};
}

This merge function now requires both parameters to be objects.

Using Type Parameters in Generic Constraints

You can also use type parameters in the constraints themselves to specify relationships between multiple parameters:

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

This ensures the key is actually a property of T.

Default Type Parameters and Using Class Types in Generics

Default type parameters and class types can also be specified, adding additional layers of precautions and versatility:

class BeeKeeper {
  hasMask: boolean = true;
}

class ZooKeeper {
  nametag: string = 'Zookeeper';
}

class Animal {
  numLegs: number;
}

class Bee extends Animal {
  keeper: BeeKeeper;
}

class Lion extends Animal {
  keeper: ZooKeeper;
}

function createInstance<A extends Animal>(c: new () => A): A {
  return new c();
}

createInstance(Lion).keeper.nametag; 
createInstance(Bee).keeper.hasMask;

This ensures the classes instantiated with createInstance have the required keeper properties.

Generic Utility Types

TypeScript also provides built-in generic utility types like Partial<T>, Readonly<T>, and Record<K, T> to provide common operations on types:

interface Todo {
  title: string;
  description: string;
}

function updateTodo(todo: Todo, fieldsToUpdate: Partial<Todo>): Todo {
  return {...todo, ...fieldsToUpdate};
}

Partial<Todo> makes all properties of Todo optional, so it can be used to update records flexibly.

Final words

In summary, TypeScript’s generics offer powerful tools to build robust, flexible, and reusable components. By mastering generics, developers can create scalable and maintainable codebases that are well-typed and safeguard against common errors.