Skip to main content

Generics in TypeScript: Write Once, Work Everywhere

You know that moment when you’re writing a function, and it hits you—“Wait, I need this same logic for strings, for users, for settings…”? And you start duplicating code like a photocopier on overdrive? That’s tech debt knocking. Generics in TypeScript aren’t some flashy gimmick. They’re the quiet fix—the “write once, work everywhere” backbone for smart, type-safe code. No hacks. No any loopholes. Just clarity with flexibility.

Let’s cut through the noise.


What Are Generics?

Generics let you write reusable code that works with any data type, while still keeping type safety.
Think of generics as templates for types—like a recipe that works for any ingredient.

Simple Example:

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

const num = identity(5); // number
const str = identity("hello"); // string
const obj = identity({ a: 1 }); // { a: number }

Here, identity works for any type you give it. TypeScript figures out the type for you.


The Magic of <T>

So, what’s the deal with <T>? That weird angle-bracket thing?

Think of T as a placeholder—a stand-in. Like “insert your type here.” It’s not a real type—yet. It’s a promise: “Hey, when you call me, tell me what you’re working with, and I’ll adjust.”

function wrapInBox<T>(item: T): { content: T } {
return { content: item };
}

const stringBox = wrapInBox("hello"); // { content: string }
const numBox = wrapInBox(42); // { content: number }
const chaosBox = wrapInBox({ wow: true }); // { content: { wow: boolean } }

No duplication. No chaos. Just clean, reusable logic. And TypeScript still knows exactly what’s inside. That’s the sweet spot.


Why Use Generics?

  • Avoid duplication: Write one function/type for many data shapes.
  • Type safety: TypeScript checks types for you, even with flexible code.
  • Reusable utilities: Functions like map, filter, or wrappers work for any type.

Constraints: When Your Types Need Rules

But wait—what if your T needs rules? Like, what if you’re sorting stuff, and it must have a name?

Enter constraints. You can limit what T can be.

interface Named {
name: string;
}

function getNames<T extends Named>(items: T[]): string[] {
return items.map(item => item.name);
}

Now, if someone tries to pass in an array of numbers? TypeScript goes: “Nah, pal. These don’t have names.” And it yells at compile time. No runtime surprises. No “undefined is not a function” at 2 a.m.


Multiple Type Parameters & Defaults

You can go full generic gymnastics. Multiple type params? Sure.

function pairUp<A, B>(first: A, second: B): [A, B] {
return [first, second];
}

pairUp("hi", 100); // [string, number]

Or even default types—like function defaults, but for types:

function createArray<T = string>(item: T, count: number): T[] {
return Array(count).fill(item);
}

createArray(5, 3); // number[]
createArray(undefined, 2); // string[] — because default kicks in

More Simple Generic Patterns

Generic Array Filter

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

const numbers = filterArray([1, 2, 3, 4], n => n > 2); // [3, 4]
const words = filterArray(["a", "bb", "ccc"], w => w.length > 1); // ["bb", "ccc"]

Generic Promise Wrapper

function toPromise<T>(value: T): Promise<T> {
return Promise.resolve(value);
}

toPromise(42).then(n => console.log(n)); // 42
toPromise("hello").then(s => console.log(s)); // "hello"

Generic Object Merge

function mergeObjects<T, U>(obj1: T, obj2: U): T & U {
return { ...obj1, ...obj2 };
}

const merged = mergeObjects({ a: 1 }, { b: "hi" }); // { a: 1, b: "hi" }

Real-World Use: API Responses

You’re building a todo app. You have fetchTodos(), fetchUsers(), fetchPosts(). All follow the same pattern: loading, data, error.

Instead of writing three nearly identical structures:

type ApiResponse<T> = {
data: T | null;
loading: boolean;
error: string | null;
};

const todoResponse: ApiResponse<Todo[]> = { data: [...], loading: false, error: null };
const userResponse: ApiResponse<User> = { data: { name: "Alex" }, loading: true, error: null };

Boom. One type. Infinite uses. Type safety intact. No copy-paste zombies haunting your codebase.


Angular Examples with Generics

Generic Service for HTTP Requests

// generic-api.service.ts
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';

export class GenericApiService {
constructor(private http: HttpClient) {}

get<T>(url: string): Observable<T> {
return this.http.get<T>(url);
}
}

Usage:

// In your component or another service
apiService.get<User[]>('/api/users').subscribe(users => {
// users is typed as User[]
});
apiService.get<Todo>('/api/todo/1').subscribe(todo => {
// todo is typed as Todo
});

Generic Angular Pipe

import { Pipe, PipeTransform } from '@angular/core';

@Pipe({ name: 'genericSort' })
export class GenericSortPipe implements PipeTransform {
transform<T>(array: T[], compareFn: (a: T, b: T) => number): T[] {
return [...array].sort(compareFn);
}
}

Usage in template:

<ul>
<li *ngFor="let item of items | genericSort:compareFn">{{ item.name }}</li>
</ul>

Generic Form Control

import { FormControl } from '@angular/forms';

function createFormControl<T>(initialValue: T): FormControl<T> {
return new FormControl<T>(initialValue);
}

const nameControl = createFormControl<string>('John');
const ageControl = createFormControl<number>(30);

When to Use Generics (and When Not To)

Use generics for:

  • Reusable utilities: map, filter, cache, result wrappers—anything that shouldn’t care about the data shape.
  • API clients: When you fetch data and want to keep type safety across different endpoints.
  • Component libraries: React/Angular props, forms, modals—pass types down like heirlooms.

Avoid generics when:

  • The type is fixed. No point over-engineering.
  • The team’s still learning TypeScript. Start simple.
  • The generic adds zero value. If it’s only used with one type—just… don’t.

The Downsides

Generics add cognitive load. For a junior dev, <T extends U where V> can look like hieroglyphs. It’s powerful—but power comes with complexity. Overuse? You end up with type soup. Wrapper<Promise<Ref<T[]>>>—what even is that?

And debugging? Sometimes the error messages are… creative. “Type ‘X’ is not assignable to type ‘infer Y in conditional over mapped tuple’.” Cool. Thanks.

Also—let’s be real—not every function needs generics. If you’re only ever dealing with strings, just use string. Don’t flex with <T> where it’s not needed. YAGNI. You ain’t gonna need it.


Additional Tips

  • Use clear, descriptive type parameter names if you have more than one (T, U, V is fine for short functions, but use Item, Result, etc. for clarity).
  • Document your generic functions and types, especially if they’re used in shared libraries.
  • Don’t be afraid to start simple and refactor to generics when you see duplication.
  • Use constraints (extends) to guide usage and prevent mistakes.

Bottom Line

Generics aren’t magic dust you sprinkle on everything. But when you need consistency across varying types—say, a function that handles user data, config objects, or API responses without turning into a typeless mess—they’re more like a master key. One key. Many doors. Each lock is different, but the mechanism? Same. You don’t rebuild the door; you design the key to adapt.

That’s the power. Use it where flexibility and safety must coexist. Not everywhere. Not always. But exactly when it clicks. And when it does? Feels less like coding, more like choreographing something that just… flows.