DEV Community

Cover image for Master Generics in TypeScript
  Isaiah   Clifford Opoku
Isaiah Clifford Opoku

Posted on

Master Generics in TypeScript

Introduction

Generics are a way to create reusable components. They allow us to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types. Generics are a way to create reusable components. They allow us to create a component that can work over a variety of types rather than a single one. This allows users to consume these components and use their own types.

Defining Generic Functions:

To declare a generic function, you need to use angle brackets (<>) to specify one or more type parameters. These type parameters act as placeholders for the actual data types that will be used when calling the function.

  • Here's the syntax for defining a generic function:
function identity<T>(arg: T): T {
    return arg;
}
Enter fullscreen mode Exit fullscreen mode

In above example, we have a function identity which returns whatever is passed to it. This function is generic because it works over a range of types.

  • We can call this function in two ways. First, we can pass all of the arguments, including the type argument, to the function:

identity<string>("myString");  // type of output will be 'string'
Enter fullscreen mode Exit fullscreen mode
  • We can als use number type instead of string type:

identity<number>(123);  // type of output will be 'number'
Enter fullscreen mode Exit fullscreen mode
  • Generic type can be inferred by the compiler which means we can omit the type argument:

identity("myString");  // type of output will be 'string'
Enter fullscreen mode Exit fullscreen mode

So now you can see with the power of generics, we can create a component that can work over a variety of types rather than a single one.

Now we will see how to use generics in functions, classes and interfaces, constraints.

Let us start with functions.

Generic Functions

Generic functions are functions that can work with a range of types. This allows us to reuse the components we build and can create a chain of functions that can work with multiple types.

Let us take an example of a function that takes an array of any type and returns the array with the same type.


// Generic function
function genericFunctions<T>(arg: T[]): T[] {
  console.log(arg.length);  // Array has a .length, so no more error
  return arg;
Enter fullscreen mode Exit fullscreen mode

In above example, we have a function genericFunctions which takes an array of any type and returns the array with the same type. We can call the function and pass an array of numbers or strings.

  • Passing an array of numbers:
// Passing an array of numbers
let outputFuncNumbers = genericFunctions<number>([1, 2, 3, 4, 5]);
console.log(outputFuncNumbers); // [1, 2, 3, 4, 5]

Enter fullscreen mode Exit fullscreen mode
  • Passing an array of strings:
// Passing an array of strings

let outputFuncString = genericFunctions<string>(["Apple", "Orange", "Banana"]);
console.log( outputFuncString); // ["Apple", "Orange", "Banana"]

Enter fullscreen mode Exit fullscreen mode

We can also omit the type argument and the compiler will infer the type for us:

  • Inferred type of String:
// Inferred type of String
let outputFuncInfer = genericFunctions(["Apple", "Orange", "Banana"]);
console.log(outputFuncInfer); // ["Apple", "Orange", "Banana"]

Enter fullscreen mode Exit fullscreen mode

I hope now you have a good understanding of generic functions. Now we will see how to use generics in classes.

Generic Classes

To declare a generic class, you use the same angle brackets (<>) syntax as with generic functions. You define the type parameter inside the class declaration and then use it throughout the class as needed.

Here's the syntax for defining a generic class:

class ClassName<T> {
  // Class properties and methods can use the type parameter 'T'
}
Enter fullscreen mode Exit fullscreen mode

Let us take an example of a generic class that takes a type parameter T and has a property of type T and a method that returns a value of type T.


class GenericClass<T> {

    // INSTANCE VARIABLE OF TYPE T
  private genericProperty: T;

// a constructor that takes an argument of type T
  constructor(value: T) {
    this.genericProperty = value;
  }

// a method that returns a value of type T
  public getGenericProperty(): T {
    return this.genericProperty;
  }
}

Enter fullscreen mode Exit fullscreen mode
  • Creating an instance of GenericClass with number and String type argument:

// Creating an instance of GenericClass
let instance1 = new GenericClass<number>(123);
console.log(instance1.getGenericProperty()); // 123 


// create instance of generic class with string type argument

let instance2 = new GenericClass<string>("Hello, world");
console.log(instance2.getGenericProperty());

Enter fullscreen mode Exit fullscreen mode

As you can see in above example, we have created an instance of GenericClass with number and String type argument. This is Mean you can create an instance of GenericClass with any type argument.

If your new to OOP in typescript, you can read my article onObject Oriented Programming with Typescript

So now you have a good understanding of generic classes. Now we will see how to use generics in interfaces.

Generic Interfaces

Generic interfaces are interfaces that can work with a range of types. This allows us to reuse the components we build and can create a chain of interfaces that can work with multiple types.Similar to generic functions and classes, you can create generic interfaces that work with different types. This allows you to define flexible interfaces that can adapt to various data structures while providing type safety.

Here's the syntax for defining a generic interface


// syntax for defining a generic interface:

interface InterfaceName<T> {
  // Interface properties and methods can use the type parameter 'T'
}

Enter fullscreen mode Exit fullscreen mode

Now let us take an example of a generic interface that takes a type parameter T and has a property of type T and a method that returns a value of type T.



interface Box<T> {
  value: T;
}

const box1: Box<number> = { value: 42 };
const box2: Box<string> = { value: "Hello" };

console.log(box1, box2); // { value: 42 } { value: 'Hello' }


Enter fullscreen mode Exit fullscreen mode

Now let see another example Generic type with multiple arguments

function genericFunction<T, U>(x: T, y: U): void {
  console.log(x, y);
}

genericFunction<number, string>(1, "Hello"); // 1 "Hello"

Enter fullscreen mode Exit fullscreen mode

Generic Functions with Interfaces:

You can also use generic functions with interfaces to define contracts for functions that can work with various data types.

interface MathOperation<T> {
  perform: (a: T, b: T) => T;
}

const add: MathOperation<number> = {
  perform: (a, b) => a + b,
};

const concatenate: MathOperation<string> = {
  perform: (a, b) => a + b,
};

console.log(add.perform(5, 10)); // Output: 15
console.log(concatenate.perform("Hello, ", "World!")); // Output: "Hello, World!"
Enter fullscreen mode Exit fullscreen mode

Now with interface you can see we can create and interface and add generic type to it and we can use it for multiple types.

So now you have a good understanding of generic interfaces. Now we will see how to use generics in constraints.

Defining Constraints:

To apply constraints, you use the extends keyword followed by the type or interface you want to use as the constraint. This tells TypeScript that the type parameter used in the generic code must be a subtype of the specified constraint.

Here's the syntax for defining constraints:

function functionName<T extends ConstraintType>(param: T): void {
  // Function logic here
}
Enter fullscreen mode Exit fullscreen mode

Example:

interface Lengthy {
  length: number;
}

function printLength<T extends Lengthy>(arg: T): void {
  console.log("Length:", arg.length);
}

printLength("Hello"); // Output: Length: 5
printLength([1, 2, 3]); // Output: Length: 3
printLength({ length: 10 }); // Output: Length: 10

printLength(42); // Error: Type 'number' does not satisfy the constraint 'Lengthy'.
Enter fullscreen mode Exit fullscreen mode

In this example, we define the Lengthy interface with a length property. The printLength function takes a generic type 'T', but it must satisfy the Lengthy constraint. Therefore, the function can be called with strings, arrays, and objects that have a length property, but it will produce a compilation error if you try to pass a type that doesn't meet the constraint (like number).

Multiple Constraints:

You can also apply multiple constraints to a generic type by using the extends keyword followed by an intersection (&) of multiple types or interfaces.

Example:

interface Printable {
  print: () => void;
}

interface Serializable {
  serialize: () => string;
}

function process<T extends Printable & Serializable>(obj: T): void {
  obj.print();
  console.log("Serialized:", obj.serialize());
}


const myObject: Printable & Serializable = {
  print() {
    console.log("Printing...");
  },
  serialize() {
    return "Serialized data";
  },
};

process(myObject); // Output: Printing... Serialized: Serialized data
Enter fullscreen mode Exit fullscreen mode

In this example, the process function takes a generic type 'T', which must satisfy both the Printable and Serializable constraints. This ensures that the function can be called only with objects that implement both interfaces. The myObject object satisfies both constraints, so it can be passed to the process function.

Using Type Constraints with Classes:

Constraints can also be applied to generic classes, ensuring that the class accepts only certain types that meet the constraint.

Example:

interface Animal {
  name: string;
}

class Zoo<T extends Animal> {
  constructor(private animals: T[]) {}

  listAnimals(): void {
    this.animals.forEach(animal => {
      console.log("Name:", animal.name);
    });
  }
}

const zoo = new Zoo([
  { name: "Lion" },
  { name: "Elephant" },
  { name: "Giraffe" },
]);

zoo.listAnimals();
Enter fullscreen mode Exit fullscreen mode

In this example, the Zoo class is a generic class that takes a type parameter 'T', which must satisfy the Animal constraint. This ensures that the array of animals passed to the constructor contains objects with the name property (which is defined in the Animal interface).

Constraints provide a powerful way to make generic code more robust and safe by specifying the types it can work with. They allow you to define clear contracts and ensure that the generic code operates only on suitable types, leading to more reliable and maintainable programs.

Thank you for getting the point of this article. I hope you enjoyed it. If you have any questions, please feel free to ask in the comments section below. I will try to answer them as soon as possible. You can find all the source code on my Github

let connect with me on Twitter and LinkedIn

Top comments (0)