img
Go back

Read

Published on: Feb 1, 2025

Programming

Understanding Typescript Generics

Generics are a powerful feature in TypeScript that allow you to create reusable and flexible components. They enable functions, classes, and interfaces to work with a variety of data types while still retaining type safety.

In this article, we’ll walk through generics in TypeScript from the basics to mid/advanced examples.


Why Use Generics?

First I’m gonna show you a real example from a project that I worked on long (waaaaay longggggg) before I knew what generics are:


type Entity = User | Transaction | Organization;

async function infinite_loader(url: string): Promise<Array<Entity>> {
  //handle the array of models inside of it.
  return entity_arr;
}

const users = await infinite_loader("/api/users?page=2") as Array<User>
const transations = await infinite_loader("/api/transations?page=3") as Array<Transation>
const organizations = await infinite_loader("/api/organizations?page=2") as Array<Organization>

Do you see the problem with this?

This function is supossed to run on the client, and it handles the infinite loader that calls the API every time the user reaches the end of the screen.

The problem with this function is that this is supossed to work for every type of model that might have to be requested to the API

So if somebody adds a new model to the codebase, then they need to get to the Entity type and modify it:


type Entity = User | Transaction | Organization | Post | Notification | ThisIsStupid | PleaseStop;

This is not scalable, it might be easy to just add a word everytime somebody adds a new model to the API, but that’s just cumbersome and stupid.

On top of that, you have to specify with the keyword “as” to get the correct type if you want the correct autocompletion.

We are not doing none of that, but let’s pretend that we don’t care about type safety, let’s just do something like this then:

async function infinite_loader(url: string): Promise<any> {
  //handle the array of models inside of it.
  return entity_arr;
}

This works, but we lose type safety—TypeScript has no idea what type “entity_arg” is, and we lose helpful features like autocomplete and error checking.

With generics, we can preserve type information, so let’s see the ideal function to our first problem:


async function infinite_loader<T>(url: string): Promise<Array<T>> {
  //handle the array of models inside of it.
  return entity_arr;
}

const users = await infinite_loader<User>("/api/users?page=2")
const transations = await infinite_loader<Transation>("/api/transations?page=3")
const organizations = await infinite_loader<Organization>("/api/organizations?page=2")

Now we’re talking!!

We have autocomplete working, the code looks clean, but more importantly: we don’t need to add anything to this file everytime somebody on Dev Ops decides to actually work and push something new to production, which let’s face it, will happen once every year or so but still, we don’t have to worry anymore.


Basic Generics

Generic Function

function echo<T>(value: T): T {
  return value;
}

const result = echo<string>("Hello TypeScript");

Here, “T” is a placeholder for the type. It could be “string”, “number”, “boolean”, a custom type, etc.

You can also let TypeScript infer the type:

const result = echo(42); // T is inferred as number

Generics with Arrays

First, if you remember the first example, you already saw a two different examples of generics already:

  Array<string | number | null>// Array themselves contain a generic value.
  Promise<Response>// Async functions return Promises, which also contain generic values.

You can make generic functions that work with arrays:

function first_element<T>(arr: T[]): T | undefined {
  return arr[0];
}

const first = first_element([1, 2, 3]); // number
const second = first_element(["a", "b"]); // string

Generic Interfaces

interface Box<T> {
  value: T;
}

const number_box: Box<number> = { value: 123 };
const string_box: Box<string> = { value: "hello" };

You can also use multiple type parameters:

interface Pair<K, V> {
  key: K;
  value: V;
}

const item: Pair<string, number> = {
  key: "age",
  value: 30,
};

Generic Classes

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

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

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

const string_container = new Container<string>();
string_container.add("hello");

Constraints with “extends”

What if we want to ensure that the generic type has a specific shape?

function print_length<T extends { length: number }>(item: T): void {
  console.log(item.length);
}

print_length("Hello"); // Works (string has length)
print_length([1, 2, 3]); // Works
// print_length(123); Error: number doesn't have a length property

This allows us to use generics but still know for a fact that if the object shape we are trying to use doesn’t contain certain values, then we are probably using the wrong object for the wrong function.


Using “keyof” with Generics

We can use “keyof” to restrict a type to a set of keys:

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

const user = { name: "Alice", age: 25 };
const name = get_property(user, "name"); // OK
// get_property(user, "nonexistent"); Error

Default Generic Types

You can provide default types for generics:

interface ApiResponse<T = any> {
  data: T;
  success: boolean;
}

const response: ApiResponse<string> = {
  data: "Done",
  success: true,
};

const defaultResponse: ApiResponse = {
  data: { message: "ok" },
  success: true,
};

Advanced: Conditional Types with Generics

You can combine generics with conditional types:

type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"

Or use them to create utilities:

type NonNullable<T> = T extends null | undefined ? never : T;

type Clean = NonNullable<string | null | undefined>; // string

Real-World Example: A Generic API Handler

interface ApiResponse<T> {
  success: boolean;
  data: T;
}

async function fetch_data<T>(url: string): Promise<ApiResponse<T>> {
  try {
    const response = await fetch(url);
    const data = await response.json() as T;
    return {
      success: true,
      data,
    }
  } catch(err) {
    return {
      success: false,
      data : null
    }
  }
}

// Usage
interface User {
  id: number;
  name: string;
}

const user = await fetch_data<User>("/api/user/1");

Conclusion

Generics in TypeScript are essential for writing clean, scalable, and type-safe code. They give your functions, classes, and interfaces the power to work with any data type while maintaining strong typing.

Start simple, experiment often, and gradually introduce generics into your real-world code.

But more importantly: Never commit the sins that I’ve commited.


type Entity = User | Transaction | KillMePlease;

You may like: