Skip to content

TypeScript Generics

Generics are one of the most powerful features in TypeScript. They allow us to write reusable, flexible components (like functions, classes, or interfaces) that can work with multiple data types, not just a single one. This greatly improves code reusability while maintaining type safety.

Why Do We Need Generics?

Imagine a simple function that takes a parameter and returns it directly.

typescript
// Using any type, but loses type information
function identity_any(arg: any): any {
    return arg;
}

// Using specific type, but cannot be reused
function identity_number(arg: number): number {
    return arg;
}

Using the any type causes the function to lose type safety because the compiler doesn't know the specific type of the return value. Writing a function for each type is very tedious. Generics were created to solve this problem.

Creating Generic Functions

We can create a generic function identity that works for all types.

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

Here, <T> is a type variable, a special kind of variable used only to represent types, not values. It captures the type passed in by the user (like number), and then we can use this type inside the function.

Using Generic Functions

There are two ways to use generic functions:

  1. Explicitly pass the type parameter:

    typescript
    let output = identity<string>("myString"); // output's type is string
  2. Type inference: The compiler automatically determines the type of T based on the passed argument.

    typescript
    let output = identity("myString"); // Compiler infers T as string, output's type is string
    let numOutput = identity(123); // Compiler infers T as number, numOutput's type is number

Generic Constraints

Sometimes, we want a generic function to work with types that have specific properties. For example, a logging function might need to access the .length property of the parameter. In this case, we can use generic constraints.

We can create an interface to describe the constraint condition, then use the extends keyword to apply it.

typescript
interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length); // Now we can safely access the .length property
    return arg;
}

loggingIdentity("hello"); // OK, string has length property
loggingIdentity([1, 2, 3]); // OK, array has length property
// loggingIdentity(123); // Error: number doesn't have length property
// loggingIdentity({ name: 'test' }); // Error: { name: string } doesn't have length property

Generic Interfaces

We can also create generic interfaces, allowing interface member types to be flexible.

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

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

let myIdentity: GenericIdentityFn<number> = identity;

console.log(myIdentity(100)); // 100
// console.log(myIdentity("test")); // Error

Generic Classes

Classes can also be generic. Generic classes use <T> after the class name to define type parameters.

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

// Generic class instance with string type
let myGenericString = new GenericNumber<string>();
myGenericString.zeroValue = "";
myGenericString.add = function(x, y) { return x + y; };

console.log(myGenericString.add("hello", " world")); // "hello world"

// Generic class instance with number type
let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function(x, y) { return x + y; };

console.log(myGenericNumber.add(10, 20)); // 30

Generics are a very important part of TypeScript's type system. Mastering them allows you to write more general, robust, and reusable code.

Content is for learning and research only.