An introduction to the utilization of Generics in TypeScript with examples grounded in real-world use cases, such as collections, approaches to error handling, the Repository Pattern, and so on. This article hopes to provide an intuitive understanding of the notion of software abstraction through Generics.

In this article, we’ll be learning the concept of Generics in TypeScript and examining how Generics can be used to write modular, decoupled, and reusable code. Along the way, we’ll briefly discuss how they fit into better testing patterns, approaches to error handling, and domain/data-access separation.

A Real-World Example

I want to enter into the world of Generics not by explaining what they are, but rather by providing an intuitive example for why they are useful. Suppose you’ve been tasked with building a feature-rich dynamic list. You could call it an array, an ArrayList, a List, a std::vector, or whatever, depending upon your language background. Perhaps this data structure must have in-built or swappable buffer systems as well (like a circular buffer insertion option). It will be a wrapper around the normal JavaScript array so that we can work with our structure instead of plain arrays.

The immediate issue you’ll come across is that of constraints imposed by the type system. You can’t, at this point, accept any type you want into a function or method in a nice clean way (we’ll revisit this statement later).

The only obvious solution is to replicate our data structure for all different types:

const intList = IntegerList.create();
intList.add(4);

const stringList = StringList.create();
stringList.add('hello');

const userList = UserList.create();
userList.add(new User('Jamie'));

The _.create()_ syntax here might look arbitrary, and indeed, _new SomethingList()_ would be more straight-forward, but you’ll see why we use this static factory method later. Internally, the _create_ method calls the constructor.

This is terrible. We have a lot of logic within this collection structure, and we’re blatantly duplicating it to support different use cases, completely breaking the DRY Principle in the process. When we decide to change our implementation, we’ll have to manually propagate/reflect those changes across all structures and types we support, including user-defined types, as in the latter example above. Suppose the collection structure itself was 100 lines long — it would be a nightmare to maintain multiple different implementations where the only difference between them is types.

An immediate solution that might come to mind, especially if you have an OOP mindset, is to consider a root “supertype” if you will. In C#, for example, there consists a type by the name of object, and object is an alias for the System.Object class. In C#’s type system, all types, be they predefined or user-defined and be they reference types or value types, inherit either directly or indirectly from System.Object. This means that any value can be assigned to a variable of type object (without getting into stack/heap and boxing/unboxing semantics).

In this case, our issue appears solved. We can just use a type like any and that will allow us to store anything we want within our collection without having to duplicate the structure, and indeed, that’s very true:

const intList = AnyList.create();
intList.add(4);

const stringList = AnyList.create();
stringList.add('hello');

const userList = AnyList.create();
userList.add(new User('Jamie'));

Let’s look at the actual implementation of our list using any:

class AnyList {
    private values: any[] = [];

    private constructor (values: any[]) {
        this.values = values;

        // Some more construction work.
    }

    public add(value: any): void {
        this.values.push(value);
    }

    public where(predicate: (value: any) => boolean): AnyList {
        return AnyList.from(this.values.filter(predicate));
    }

    public select(selector: (value: any) => any): AnyList {
        return AnyList.from(this.values.map(selector));
    }

    public toArray(): any[] {
        return this.values;
    }

    public static from(values: any[]): AnyList {
        // Perhaps we perform some logic here.
        // ...

        return new AnyList(values);
    }

    public static create(values?: any[]): AnyList {
        return new AnyList(values ?? []);
    }

    // Other collection functions.
    // ...
}

#typescript #javascript #web-development #programming #developer

Understanding TypeScript Generics
2.55 GEEK