Learn TypeScript With Me in 14 Days Day 7

Posted on 7 October 2024 Reading time: 5 min read
Learn TypeScript With Me in 14 Days Day 7

Day 7: Generics in TypeScript

Understanding Generics in TypeScript

In this part of the series, we will learn about Generics in TypeScript, a powerful feature that allows us to create reusable components, functions, and classes that work with different types while still maintaining type safety. By the end of this post, we will understand how to use generics in our code and apply them to our ongoing Task Manager project.

Why Use Generics?

Generics allow us to write code that is flexible and reusable, without sacrificing type safety. Instead of hardcoding a specific type, generics let you define placeholders for types that can be substituted when the function, class, or component is used. This makes it easier to create reusable code that works with a wide variety of data types.

Understanding Generics with Functions

Let’s start by defining a simple generic function that works with different types of data. A common use case for generics is a function that accepts an array of any type and returns the first element.

Example of a Generic Function:

function getFirstElement<T>(arr: T[]): T {
  return arr[0];
}

const firstNumber = getFirstElement<number>([1, 2, 3]);
console.log(firstNumber); // Output: 1

const firstString = getFirstElement<string>(['TypeScript', 'JavaScript']);
console.log(firstString); // Output: TypeScript

Explanation:

  • The T is a generic type parameter. It acts as a placeholder for the actual type that will be provided when the function is called.
  • Type Safety: The function is type-safe, ensuring that the type of the input array and the return value match.
  • Reusability: The same function can be used for different types, such as arrays of numbers or strings.

In this example, we use getFirstElement for both an array of numbers and an array of strings without having to rewrite the function.

Using Generics with Arrays

You can use generics to create utility functions that work with arrays of any type. Let’s create a function that returns the last element of an array using generics.

Example: Get Last Element of an Array

function getLastElement<T>(arr: T[]): T {
  return arr[arr.length - 1];
}

const lastNumber = getLastElement<number>([10, 20, 30]);
console.log(lastNumber); // Output: 30

const lastString = getLastElement<string>(['apple', 'banana', 'cherry']);
console.log(lastString); // Output: cherry

Here, we can reuse the same logic for arrays of any type, further demonstrating how generics helps avoid duplication in code.

Generics with Classes

Generics can also be applied to classes to make them more flexible. Let’s create a generic class that can manage a list of items, regardless of the type of the items.

Example of a Generic Class:

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

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

  getAllItems(): T[] {
    return this.items;
  }
}

const numberList = new List<number>();
numberList.addItem(42);
numberList.addItem(84);
console.log(numberList.getAllItems()); // Output: [42, 84]

const stringList = new List<string>();
stringList.addItem('TypeScript');
stringList.addItem('JavaScript');
console.log(stringList.getAllItems()); // Output: ['TypeScript', 'JavaScript']

Explanation:

  • List: This generic class works with any type T. Whether it’s numbers, strings, or custom objects, this class manages a list of items of that type.
  • Flexibility: The class can now be reused to manage a list of any data type.

Applying Generics to the Task Manager Project

Let’s see how we can apply generics to our Task Manager project to make it more flexible and reusable.

1. Create a Generic List Manager for Tasks

We can create a generic class to manage different lists of tasks. Here’s how:

class TaskManager<T> {
  private tasks: T[] = [];

  addTask(task: T): void {
    this.tasks.push(task);
  }

  getTasks(): T[] {
    return this.tasks;
  }
}

2. Use TaskManager to Manage Tasks

Now, we can use this generic TaskManager to manage our list of tasks.

import { Task } from './task';

const taskManager = new TaskManager<Task>();

taskManager.addTask(new Task('Learn TypeScript'));
taskManager.addTask(new Task('Build Task Manager'));

console.log(taskManager.getTasks().map(task => task.getDetails()));

Explanation:

  • TaskManager: We use TaskManager with the Task type to manage a list of tasks in the project.
  • This allows for type safety while still keeping the code flexible enough to work with other types if needed.

Constraints in Generics

In some cases, you may want to restrict the types that can be used in a generic. You can do this using constraints.

Example: Constraining a Generic to an Interface

Let’s say we want to create a generic function that only works with objects that have an id property. We can use a constraint to enforce this requirement.

interface Identifiable {
  id: number;
}

function findById<T extends Identifiable>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const tasks = [
  { id: 1, title: 'Learn TypeScript' },
  { id: 2, title: 'Build Task Manager' }
];

const foundTask = findById(tasks, 2);
console.log(foundTask); // Output: { id: 2, title: 'Build Task Manager' }

Explanation:

  • T extends Identifiable**: This ensures that T must be an object with an id property.
  • Type Safety: The function will only work with types that adhere to the Identifiable interface.

Generics with Multiple Type Parameters

You can also use multiple type parameters in a generic function or class. Let’s create a function that pairs two values of different types.

Example: Pairing Two Values with Generics

function pair<U, V>(first: U, second: V): [U, V] {
  return [first, second];
}

const stringNumberPair = pair<string, number>('TypeScript', 2024);
console.log(stringNumberPair); // Output: ['TypeScript', 2024]

const booleanStringPair = pair<boolean, string>(true, 'Success');
console.log(booleanStringPair); // Output: [true, 'Success']

Explanation:

  • <U, V>: The function accepts two generic type parameters (U and V), allowing us to work with two different types.
  • Tuple Return Type: The function returns a tuple containing both values.

What’s Next?

Today, we have learnt how to use Generics in TypeScript to create flexible and reusable functions, classes, and components. With Generics, we can write code that works with a variety of types while still ensuring type safety.

In the next part of the series, we will explore Modules in TypeScript, which will enable us organise our code into reusable and maintainable pieces. If you have any questions or comments, feel free contact me at steve [at] stevepopoola.uk.

Useful Resources