Vincent Ombogo
AboutServicesProjectsBlogTestimonialsContact
Background grid pattern

Let's work together

Interested in collaborating? Reach out and let's build something great.

Copyright © 2026 Vincent Ombogo

Back to Blog
Understanding TypeScript Generics: From Basics to Advanced Patterns
TypeScriptJavaScriptProgrammingWeb Development

Understanding TypeScript Generics: From Basics to Advanced Patterns

A comprehensive guide to TypeScript generics — covering everything from simple type parameters to advanced patterns like conditional types, mapped types, and template literals.

Vincent OmbogoJuly 10, 202612 min read

Why Generics Matter

TypeScript generics are one of the most powerful features of the language. They allow you to write reusable, type-safe code that works with multiple types while maintaining the benefits of static typing.

Think of generics as type variables — they let you capture the type of an argument so you can use it throughout your function, class, or interface.

Getting Started with Generics

The Problem Generics Solve

Without generics, you'd have to either use any (losing type safety) or create duplicate functions for each type:

hljs typescript
// ❌ Using any — no type safety
function identity(arg: any): any {
  return arg;
}

// ❌ Duplicated for each type
function identityNumber(arg: number): number {
  return arg;
}
function identityString(arg: string): string {
  return arg;
}

The Generic Solution

hljs typescript
// ✅ Generic — preserves type information
function identity<T>(arg: T): T {
  return arg;
}

const num = identity(42); // type: number
const str = identity("hello"); // type: string
const bool = identity(true); // type: boolean

Generic Constraints

Sometimes you need to restrict what types can be used with a generic. That's where constraints come in:

hljs typescript
interface HasLength {
  length: number;
}

// T must have a length property
function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello"); // ✅ string has length
logLength([1, 2, 3]); // ✅ array has length
logLength({ length: 10 }); // ✅ object with length
// logLength(42); // ❌ number doesn't have length

Using keyof with Constraints

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

const user = { name: "Vincent", age: 25, role: "developer" };
const name = getProperty(user, "name"); // type: string
const age = getProperty(user, "age"); // type: number
// getProperty(user, "email"); // ❌ Property 'email' doesn't exist

Generic Interfaces and Types

Generic Interface

hljs typescript
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

interface User {
  id: string;
  name: string;
  email: string;
}

type UserResponse = ApiResponse<User>;
// {
//   data: User;
//   status: number;
//   message: string;
// }

Generic Type Aliases

hljs typescript
type Result<T, E = Error> =
  | { success: true; value: T }
  | { success: false; error: E };

const successResult: Result<number> = {
  success: true,
  value: 42,
};

const errorResult: Result<string> = {
  success: false,
  error: new Error("Something went wrong"),
};

Advanced Generic Patterns

Conditional Types

Conditional types allow you to create types that depend on a condition:

hljs typescript
type IsString<T> = T extends string ? true : false;

type A = IsString<string>; // true
type B = IsString<number>; // false

Inferring Types

The infer keyword lets you extract types from other types:

hljs typescript
// Extract the return type of a function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Fn = (a: number, b: string) => boolean;
type FnReturn = ReturnType<Fn>; // boolean

// Extract promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;

type Promised = Promise<string>;
type Unwrapped = Unwrap<Promised>; // string

Mapped Types

Mapped types let you transform existing types:

hljs typescript
type Readonly<T> = {
  readonly [K in keyof T]: T[K];
};

type Optional<T> = {
  [K in keyof T]?: T[K];
};

type Nullable<T> = {
  [K in keyof T]: T[K] | null;
};

interface User {
  name: string;
  age: number;
}

type ReadonlyUser = Readonly<User>;
// {
//   readonly name: string;
//   readonly age: number;
// }

Template Literal Types

hljs typescript
type EventName = `on${Capitalize<string>}`;

type EventHandler<T extends EventName> = T extends `on${infer E}`
  ? (event: E) => void
  : never;

type ClickHandler = EventHandler<"onClick">; // (event: "Click") => void

Practical Examples

Type-Safe API Client

hljs typescript
interface ApiConfig {
  baseUrl: string;
  headers?: Record<string, string>;
}

class ApiClient {
  constructor(private config: ApiConfig) {}

  async get<T>(path: string): Promise<T> {
    const response = await fetch(`${this.config.baseUrl}${path}`, {
      headers: this.config.headers,
    });
    return response.json();
  }

  async post<T, U>(path: string, body: U): Promise<T> {
    const response = await fetch(`${this.config.baseUrl}${path}`, {
      method: "POST",
      headers: {
        "Content-Type": "application/json",
        ...this.config.headers,
      },
      body: JSON.stringify(body),
    });
    return response.json();
  }
}

const api = new ApiClient({ baseUrl: "https://api.example.com" });

// Types are inferred automatically
const user = await api.get<User>("/users/1");
const created = await api.post<User, CreateUserDto>("/users", {
  name: "Vincent",
  email: "vincent@example.com",
});

Generic React Component

hljs tsx
interface ListProps<T> {
  items: T[];
  renderItem: (item: T, index: number) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item, index) => (
        <li key={keyExtractor(item)}>{renderItem(item, index)}</li>
      ))}
    </ul>
  );
}

// Usage
<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>;

Best Practices

  1. Use descriptive names — T is fine for simple cases, but use TItem, TResponse, etc., for clarity
  2. Constrain when needed — don't over-constrain, but don't leave types too loose
  3. Prefer inference — let TypeScript infer generic types when possible
  4. Keep it simple — don't over-engineer with complex generics if a simpler solution works

Conclusion

Generics are what make TypeScript truly powerful. They enable you to write flexible, reusable code without sacrificing type safety. Start simple, and as you get comfortable, explore the more advanced patterns. Your codebase will thank you.


What's your favorite TypeScript feature? I regularly share TypeScript tips on Twitter. Follow me for more.

Share this article:

Related Articles

Building Performant React Applications: A Practical Guide
ReactPerformance

Building Performant React Applications: A Practical Guide

Learn the essential techniques for building fast, responsive React applications — from code splitting and lazy loading to memoization and state management optimization.

Jul 15, 202610 min read
Why Every Small and Medium Business Needs a Professional Website in 2026
BusinessSMEs

Why Every Small and Medium Business Needs a Professional Website in 2026

Discover how a modern website helps small and medium businesses attract more customers, build trust, and increase sales

Aug 5, 20264 min read
Getting Started with Next.js 16: What's New and Why You Should Care
Next.jsReact

Getting Started with Next.js 16: What's New and Why You Should Care

Next.js 16 brings groundbreaking features including the Turbopack stable release, React 19 support, and enhanced server actions. Here's everything you need to know.

Jul 20, 20268 min read