Tutorials Logic, IN info@tutorialslogic.com
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Website Development
Practice
Quiz Challenge Interview Questions Certification Practice
Tools
Online Compiler JSON Formatter Regex Tester CSS Unit Converter Color Picker
Compiler Tools

TypeScript Generics: Reusable Type-Safe Functions and Components

What Are Generics?

Generics let a type be supplied later, at the point where a function, type, interface, or class is used. Instead of writing separate logic for strings, numbers, users, products, and every other value shape, you write one reusable structure that preserves the exact type relationship.

The most important idea is this: generics do not mean "anything with no checking". They mean "this code works with many types, and TypeScript should remember which exact type is being used right now." That is why generics are safer than using any.

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

const name = identity<string>("Asha");
const score = identity<number>(95);
const active = identity(true); // TypeScript infers boolean

console.log(name.toUpperCase());
console.log(score.toFixed(1));
console.log(active ? "yes" : "no");

Why Not Use any?

any removes type checking. A function that accepts and returns any may look flexible, but callers lose information about the value they passed in. Generics keep flexibility without throwing away safety.

A generic function says, "give me a value of some type, and I will return that same type." An any function says, "give me something, and I will return something unchecked." That difference matters as a codebase grows.

Generic Safety
function firstAny(items: any[]): any {
  return items[0];
}

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

const unsafeUser = firstAny([{ name: "Admin" }]);
unsafeUser.notARealMethod(); // No compiler warning because it is any

const safeUser = first([{ name: "Admin" }]);
// safeUser.notARealMethod(); // Error
console.log(safeUser?.name);

Generic Constraints

A constraint limits what a generic type can be. This is useful when the function needs to access a property or method. Without a constraint, TypeScript only knows that T could be anything, so it will not allow property access.

Use extends to say that a generic type must have a minimum shape. The caller can still pass extra properties, but the required properties must exist.

PatternMeaning
<T>Any type can be supplied.
<T extends string>Only string-like values are allowed.
<T extends { id: number }>The value must have an id property.
extends Constraint
function getLabel<T extends { label: string }>(item: T): string {
  return item.label;
}

const course = getLabel({ id: 1, label: "TypeScript" });
const product = getLabel({ sku: "BK-10", label: "Book", price: 299 });

console.log(course);
console.log(product);

// getLabel({ id: 2 });
// Error: label is required by the constraint.

Generic Types and Interfaces

Generics are not only for functions. You can use them in type aliases and interfaces to describe reusable containers. This is common for API responses, pagination, form state, table rows, dropdown options, and cache entries.

The container shape stays the same, while the inner data type changes per use case.

Generic API Response
type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
};

type User = { id: number; name: string };
type BlogPost = { id: number; title: string; slug: string };

const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Admin" },
  status: 200,
  message: "OK",
};

const postResponse: ApiResponse<BlogPost[]> = {
  data: [{ id: 10, title: "Generics", slug: "generics" }],
  status: 200,
  message: "OK",
};

Generic Classes

A class can also be generic. This is useful when a class stores, manages, or transforms values but should not care about the exact item type. Examples include repositories, queues, collections, caches, and event emitters.

When you create an instance, the generic type tells TypeScript what values the instance can accept and return.

Generic Repository
type Entity = { id: number };

class Repository<T extends Entity> {
  private items: T[] = [];

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

  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }
}

const users = new Repository<{ id: number; name: string }>();
users.add({ id: 1, name: "Asha" });

console.log(users.findById(1)?.name);

When to Use Generics

Use generics when there is a real relationship between types. A function that accepts a value and returns the same value is a good fit. A response wrapper that contains different data per endpoint is a good fit. A dropdown component that receives options and returns the selected option is also a good fit.

Avoid adding generics just to look advanced. If a function only ever works with one concrete type, a normal type annotation is clearer.

  • Use generics for reusable data containers.
  • Use generics when input and output types must stay connected.
  • Use constraints when the generic value must have required properties.
  • Prefer inference when TypeScript can understand the generic type from arguments.
Key Takeaways
  • Generics create reusable code while preserving exact type information.
  • Generics are safer than any because callers keep their specific types.
  • Constraints with extends let generic code safely use required properties.
  • Generic type aliases and interfaces are common for API responses, state, and reusable components.
  • Use generics when type relationships matter, not as decoration.

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.