TypeScript Generic Array: A Complete Guide

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

Introduction

In the well-trod land of TypeScript, a versatile creation known as a generic array glimmers with the promise of type safety and flexibility. This guide offers to elucidate the mysteries of generic arrays, delivering ye an ample understanding of their potent capabilities through a cavalcade of code exemplars.

What are Generics?

Generics imbue TypeScript with the capacity to create components that can work over a variety of types rather than a single one. Imagine them as tools equally adept at carving both the hardy oak and the supple willow, with a precision that belies a tailor’s skill.

function getArray<T>(items : T[] ) : T[] {
    return new Array<T>().concat(items);
}

let numberArray = getArray<number>([1, 2, 3]);
let stringArray = getArray<string>(['hello', 'world']);

Basic Generic Array Usage

Our journey begins with the simplest manifestation of a generic array, one that moonlights as a repository for any type of artifact, from numbers to strings to the mysterious objects.

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

let output = identity('Mark Twain');  // output will be of type 'string'
console.log(output);

Generic Interfaces

As we progress, we shall encounter generic interfaces, steadfast contracts that bind the properties of our creations to our will.

interface GenericIdentityFn<T> {
    (arg: T): T;
}

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

let myIdentity: GenericIdentityFn<number> = identity;

Using Type Parameters

Next, type parameters take the stage, leading us to pen functions enlightened by the foresight of types known only in the murky future.

function loggingIdentity<T>(arg: T[] ): T[] {
    console.log(arg.length);  // Array has a .length, so no more error
    return arg;
}

Generic Constraints

We then wade into the realm of constraints, where our generic types are not as unfettered as before, now bound by the attributes of another, as in a lively jig betwixt old friends.

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);  // This works now
    return arg;
}

The Specter of Handling Multiple Types

Indeed, our journey grows more thrilling yet, as we cast spells powerful enough to tame not one, but a dance of multiple differing types, swirling in harmony within a single function.

function combine<T, U>(arg1: T, arg2: U): [T, U] {
    return [arg1, arg2];
}

let result = combine(5, 'times');
console.log(result);  // Output: [5, 'times']

Working with Generic Classes

Before long, we heed the call of generic classes, where the principles we’ve hitherto grasped illuminate the path to creating structures flexible and resilient—a pantheon for variables of any genre.

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

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

Conditional Types and Arrays

In the twilight of our expedition, conditional types beckon us to open doors to deeper wisdom, where types change their guise like the capricious river, ever-dynamic, responsive to the input conveyed by our hands.

type StringOrNumber<T> = T extends number ? number[] : string[];

function process(arg: T): StringOrNumber<T> {
    if (typeof arg === 'number') {
        return [arg]; // number[]
    } else {
        return [arg.toString()]; // string[]
    }
}

Conclusion

As the last light fades on our sublime sojourn, remember that generics in TypeScript proffer, not just wisdom in the array of types, but an expanse that spurns the rigid for the dynamic. Abide by the principles laid forth, and traverse the type-filled lands with both confidence and grace.