TypeScript Generic Function: A Complete Guide

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

Introduction

Unlock the power of type-safe code with TypeScript Generic Functions. This guide will walk you through the fundamentals to advanced usage, enhancing your toolbox for scalable and maintainable programming.

TypeScript generic functions are a core feature of the language, enabling developers to write flexible, reusable code that is also safe at compile-time. By utilizing generics, programs can handle different types without sacrificing type safety or needing to duplicate code for each distinct type. In this tutorial, we will explore TypeScript generic functions from basics to advanced patterns, accompanied by practical code examples.

Getting Started with Generic Functions

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

let output1 = identity<string>("myString");
let output2 = identity<number>(100);

The function identity is a simple example that takes an argument and returns it. By defining <T>, we declare a type variable that can be replaced with any type, provided at invocation, without losing type information.

Working with Constraints

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    return arg;
}

loggingIdentity({length: 10}); // OK
// loggingIdentity(3); // Error: number doesn't have a .length property

Constraints enforce that the type passed to our generic function adheres to a certain structure. Here, T must have a property length, providing compile-time assurance that calling properties or methods on arg is valid.

Using Type Parameters in Generic Constraints

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

let x = { a: 1, b: 2, c: 3 };

getProperty(x, "a"); // OK
// getProperty(x, "m"); // Error: 'm' isn't a key in 'x'

In the getProperty function, we declare a constraint K that must be a key of T, allowing us to safely fetch properties from objects.

Using Class Types in Generics

class BeeKeeper {
    hasMask: boolean = true;
}

class ZooKeeper {
    nameTag: string = "Mikko";
}

class Animal {
    numLegs: number = 4;
}

class Bee extends Animal {
    keeper: BeeKeeper = new BeeKeeper();
}

class Lion extends Animal {
    keeper: ZooKeeper = new ZooKeeper();
}

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

createInstance(Bee).keeper.hasMask; // OK
// createInstance(Lion).keeper.hasMask; // Error: Property 'hasMask' does not exist on type 'ZooKeeper'.

The use of the new() syntax in the type of the parameter c indicates that the argument for c must be a class that can be instantiated.

Advanced Patterns

As you get more comfortable with generics, you can explore more complex patterns like using them with higher order functions, async/await, and more.

Generics with Higher Order Functions

function filter<T>(arr: T[], predicate: (val: T) => boolean): T[] {
    return arr.filter(predicate);
}

filter([1, 2, 3, 4], (val) => val > 2); // returns [3, 4]
filter(['apple', 'banana', 'grape'], (val) => val.startsWith('a')); // returns ['apple']

This versatile filter function works with arrays of any type, leveraging the power of generics to maintain type safety throughout various operations on arrays.

Generics in Async Functions

async function fetchAndGetResponse<T>(url: string): Promise<T> {
    const response = await fetch(url);
    return response.json();
}

// Usage with an interface for the expected JSON structure
interface User {
    id: number;
    name: string;
}

fetchAndGetResponse<User>('https://api.example.com/users/1')
    .then((user: User) => console.log(user.name));

Using generics with async functions and promises allows your functions to return specific types while working with asynchronous tasks like fetching data from a REST API.

Conclusion

Generics in TypeScript enable developers to write safer and more versatile functions, leading to cleaner codebases. You have seen how generic functionality can be implemented from basic identity functions to advanced type constraints, which broadens your coding capability and allows you to handle dynamic and complex coding scenarios with ease. Embrace generics to produce robust TypeScript code that scales.