Falytom logoFALYTOM
Games
ToolsUI ComponentsServicesProductsAbout
menu
  1. Home
  2. /Web Development
  3. /Typescript
  4. /Generics Introduction
Your Progress0%
0 of 30 completed

TypeScript Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices

Introduction to Generics

Creating reusable, type-safe components with generic types

Generics are one of TypeScript's most powerful features. They let you create reusable components that work with multiple types while maintaining type safety. Instead of writing separate functions for each type or using any, generics let you write one function that works with any type—and TypeScript still knows what type you're working with! Let's master generics!

Why Generics?

Imagine you need a function that returns the first element of an array. Without generics, you have two bad options:

why-generics.ts
// Bad Option 1: Use 'any' (loses type safety)
function getFirst_any(arr: any[]): any {
  return arr[0];
}

const numbers = [1, 2, 3];
const first = getFirst_any(numbers);
// Type of 'first' is 'any' - no type safety!
// first.toUpperCase();  // Compiles but crashes at runtime

// Bad Option 2: Write separate functions for each type
function getFirstNumber(arr: number[]): number | undefined {
  return arr[0];
}

function getFirstString(arr: string[]): string | undefined {
  return arr[0];
}

function getFirstBoolean(arr: boolean[]): boolean | undefined {
  return arr[0];
}
// ... need a new function for every type!

// Good Solution: Generics!
function getFirst<T>(arr: T[]): T | undefined {
  return arr[0];
}

const nums = [1, 2, 3];
const firstNum = getFirst(nums);  // Type: number | undefined ✅

const strs = ["hello", "world"];
const firstStr = getFirst(strs);  // Type: string | undefined ✅

const bools = [true, false];
const firstBool = getFirst(bools);  // Type: boolean | undefined ✅

// One function, works with all types, fully type-safe!
if (firstNum !== undefined) {
  console.log(firstNum.toFixed(2));  // ✅ Works
}

if (firstStr !== undefined) {
  console.log(firstStr.toUpperCase());  // ✅ Works
}

💡 Generics = Reusability + Type Safety

Generics let you write code once that works with many types, while preserving complete type safety. They're like function parameters, but for types!

Basic Generic Functions

Generic functions use type parameters (usually T) to work with any type while maintaining type safety.

Simple Generic Functions

generic-functions-basic.ts
// Identity function - returns what you pass in
function identity<T>(arg: T): T {
  return arg;
}

// TypeScript infers the type
const num = identity(42);         // Type: number
const str = identity("hello");    // Type: string
const bool = identity(true);      // Type: boolean
const obj = identity({ x: 10 });  // Type: { x: number }

// You can explicitly specify the type
const explicit = identity<string>("world");  // Type: string

// Array wrapper
function wrapInArray<T>(value: T): T[] {
  return [value];
}

const nums = wrapInArray(5);           // Type: number[]
const strs = wrapInArray("hello");     // Type: string[]
const objs = wrapInArray({ id: 1 });   // Type: { id: number }[]

console.log(nums);  // [5]
console.log(strs);  // ["hello"]

// Get last element
function getLast<T>(arr: T[]): T | undefined {
  return arr[arr.length - 1];
}

const lastNum = getLast([1, 2, 3]);        // Type: number | undefined
const lastStr = getLast(["a", "b", "c"]); // Type: string | undefined

console.log(lastNum);  // 3
console.log(lastStr);  // "c"

// Swap function
function swap<T, U>(tuple: [T, U]): [U, T] {
  return [tuple[1], tuple[0]];
}

const original: [string, number] = ["hello", 42];
const swapped = swap(original);  // Type: [number, string]

console.log(original);  // ["hello", 42]
console.log(swapped);   // [42, "hello"]

Generic Functions with Multiple Type Parameters

multiple-type-parameters.ts
// Two type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair("name", "Yusuf");     // Type: [string, string]
const p2 = pair("age", 25);            // Type: [string, number]
const p3 = pair(true, [1, 2, 3]);      // Type: [boolean, number[]]

console.log(p1);  // ["name", "Yusuf"]
console.log(p2);  // ["age", 25]

// Map function
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

const numbers = [1, 2, 3, 4, 5];
const doubled = map(numbers, (n) => n * 2);
// Type: number[]

const stringified = map(numbers, (n) => `Number: ${n}`);
// Type: string[]

const objects = map(numbers, (n) => ({ value: n }));
// Type: { value: number }[]

console.log(doubled);      // [2, 4, 6, 8, 10]
console.log(stringified);  // ["Number: 1", "Number: 2", ...]
console.log(objects);      // [{ value: 1 }, { value: 2 }, ...]

// Merge objects
function merge<T, U>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const person = { name: "Yusuf", age: 25 };
const contact = { email: "yusuf@example.com", phone: "+234-803-123-4567" };

const fullProfile = merge(person, contact);
// Type: { name: string; age: number } & { email: string; phone: string }

console.log(fullProfile);
// { name: "Yusuf", age: 25, email: "yusuf@example.com", phone: "+234-803-123-4567" }

console.log(fullProfile.name);   // "Yusuf"
console.log(fullProfile.email);  // "yusuf@example.com"

Generic Interfaces

Interfaces can also use generics to define reusable, type-safe structures.

generic-interfaces.ts
// Generic interface
interface Container<T> {
  value: T;
  getValue(): T;
  setValue(value: T): void;
}

// Using with different types
const numberContainer: Container<number> = {
  value: 42,
  getValue() {
    return this.value;
  },
  setValue(value: number) {
    this.value = value;
  }
};

const stringContainer: Container<string> = {
  value: "hello",
  getValue() {
    return this.value;
  },
  setValue(value: string) {
    this.value = value;
  }
};

console.log(numberContainer.getValue());  // 42
numberContainer.setValue(100);
console.log(numberContainer.getValue());  // 100

// Generic response interface
interface ApiResponse<T> {
  success: boolean;
  data: T;
  timestamp: Date;
}

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

interface Product {
  id: number;
  name: string;
  price: number;
}

const userResponse: ApiResponse<User> = {
  success: true,
  data: {
    id: 1,
    name: "Yusuf Ibrahim",
    email: "yusuf@example.com"
  },
  timestamp: new Date()
};

const productResponse: ApiResponse<Product> = {
  success: true,
  data: {
    id: 101,
    name: "Laptop",
    price: 450000
  },
  timestamp: new Date()
};

console.log(userResponse.data.name);      // "Yusuf Ibrahim"
console.log(productResponse.data.price);  // 450000

// Generic with multiple type parameters
interface Pair<K, V> {
  key: K;
  value: V;
}

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

const numberBoolean: Pair<number, boolean> = {
  key: 1,
  value: true
};

console.log(`${stringNumber.key}: ${stringNumber.value}`);  // "age: 25"

Generic Classes

Classes can use generics to create reusable, type-safe data structures and utilities.

generic-classes.ts
// Generic class
class Box<T> {
  private content: T;
  
  constructor(value: T) {
    this.content = value;
  }
  
  getValue(): T {
    return this.content;
  }
  
  setValue(value: T): void {
    this.content = value;
  }
}

// Create boxes with different types
const numberBox = new Box<number>(42);
console.log(numberBox.getValue());  // 42
numberBox.setValue(100);
console.log(numberBox.getValue());  // 100

const stringBox = new Box<string>("hello");
console.log(stringBox.getValue());  // "hello"
stringBox.setValue("world");
console.log(stringBox.getValue());  // "world"

// Type inference works too
const autoBox = new Box(true);  // TypeScript infers Box<boolean>
console.log(autoBox.getValue());  // true

// Generic stack
class Stack<T> {
  private items: T[] = [];
  
  push(item: T): void {
    this.items.push(item);
  }
  
  pop(): T | undefined {
    return this.items.pop();
  }
  
  peek(): T | undefined {
    return this.items[this.items.length - 1];
  }
  
  isEmpty(): boolean {
    return this.items.length === 0;
  }
  
  size(): number {
    return this.items.length;
  }
}

const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);

console.log(numberStack.pop());   // 3
console.log(numberStack.peek());  // 2
console.log(numberStack.size());  // 2

const stringStack = new Stack<string>();
stringStack.push("first");
stringStack.push("second");

console.log(stringStack.pop());   // "second"
console.log(stringStack.peek());  // "first"

// Generic key-value store
class KeyValueStore<K, V> {
  private store = new Map<K, V>();
  
  set(key: K, value: V): void {
    this.store.set(key, value);
  }
  
  get(key: K): V | undefined {
    return this.store.get(key);
  }
  
  has(key: K): boolean {
    return this.store.has(key);
  }
  
  delete(key: K): boolean {
    return this.store.delete(key);
  }
  
  getAll(): [K, V][] {
    return Array.from(this.store.entries());
  }
}

const userStore = new KeyValueStore<number, string>();
userStore.set(1, "Yusuf");
userStore.set(2, "Amina");
userStore.set(3, "Chidi");

console.log(userStore.get(1));  // "Yusuf"
console.log(userStore.has(2));  // true
console.log(userStore.getAll());  // [[1, "Yusuf"], [2, "Amina"], [3, "Chidi"]]

Working with Generic Arrays

generic-arrays.ts
// Generic array utilities
function toArray<T>(...items: T[]): T[] {
  return items;
}

const nums = toArray(1, 2, 3, 4, 5);         // Type: number[]
const strs = toArray("a", "b", "c");         // Type: string[]
const mixed = toArray(1, "two", 3, "four");  // Type: (string | number)[]

console.log(nums);   // [1, 2, 3, 4, 5]
console.log(mixed);  // [1, "two", 3, "four"]

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

const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const evens = filter(numbers, (n) => n % 2 === 0);
const greaterThanFive = filter(numbers, (n) => n > 5);

console.log(evens);             // [2, 4, 6, 8, 10]
console.log(greaterThanFive);   // [6, 7, 8, 9, 10]

// Find in array
function find<T>(arr: T[], predicate: (item: T) => boolean): T | undefined {
  return arr.find(predicate);
}

const users = [
  { id: 1, name: "Yusuf", age: 25 },
  { id: 2, name: "Amina", age: 30 },
  { id: 3, name: "Chidi", age: 28 }
];

const user = find(users, (u) => u.id === 2);
console.log(user);  // { id: 2, name: "Amina", age: 30 }

// Reduce array
function reduce<T, U>(
  arr: T[],
  reducer: (acc: U, item: T) => U,
  initial: U
): U {
  return arr.reduce(reducer, initial);
}

const sum = reduce(numbers, (acc, n) => acc + n, 0);
console.log(sum);  // 55

const names = reduce(users, (acc, user) => [...acc, user.name], [] as string[]);
console.log(names);  // ["Yusuf", "Amina", "Chidi"]

// Chunk array
function chunk<T>(arr: T[], size: number): T[][] {
  const chunks: T[][] = [];
  for (let i = 0; i < arr.length; i += size) {
    chunks.push(arr.slice(i, i + size));
  }
  return chunks;
}

const items = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const chunked = chunk(items, 3);
console.log(chunked);  // [[1, 2, 3], [4, 5, 6], [7, 8, 9]]

const words = ["apple", "banana", "cherry", "date", "elderberry"];
const wordChunks = chunk(words, 2);
console.log(wordChunks);  // [["apple", "banana"], ["cherry", "date"], ["elderberry"]]

Practical Examples

Example 1: Data Fetcher

data-fetcher.ts
// Generic data fetcher
class DataFetcher<T> {
  private cache = new Map<string, T>();
  
  async fetch(url: string): Promise<T> {
    // Check cache first
    if (this.cache.has(url)) {
      console.log("Returning from cache");
      return this.cache.get(url)!;
    }
    
    console.log("Fetching from API");
    const response = await fetch(url);
    const data = await response.json() as T;
    
    // Cache the result
    this.cache.set(url, data);
    
    return data;
  }
  
  clearCache(): void {
    this.cache.clear();
  }
}

// User type
interface User {
  id: number;
  name: string;
  email: string;
}

// Product type
interface Product {
  id: number;
  name: string;
  price: number;
}

// Create typed fetchers
const userFetcher = new DataFetcher<User>();
const productFetcher = new DataFetcher<Product>();

// Use with full type safety
async function getUser(id: number): Promise<void> {
  const user = await userFetcher.fetch(`/api/users/${id}`);
  // TypeScript knows 'user' is User type
  console.log(`User: ${user.name}`);
  console.log(`Email: ${user.email}`);
}

async function getProduct(id: number): Promise<void> {
  const product = await productFetcher.fetch(`/api/products/${id}`);
  // TypeScript knows 'product' is Product type
  console.log(`Product: ${product.name}`);
  console.log(`Price: ₦${product.price}`);
}

// Usage
getUser(1);
getProduct(101);

Example 2: Repository Pattern

repository-pattern.ts
// Generic repository for CRUD operations
interface Entity {
  id: number;
}

class Repository<T extends Entity> {
  private items: T[] = [];
  private nextId = 1;
  
  create(item: Omit<T, "id">): T {
    const newItem = { ...item, id: this.nextId++ } as T;
    this.items.push(newItem);
    return newItem;
  }
  
  findById(id: number): T | undefined {
    return this.items.find(item => item.id === id);
  }
  
  findAll(): T[] {
    return [...this.items];
  }
  
  update(id: number, updates: Partial<T>): T | undefined {
    const index = this.items.findIndex(item => item.id === id);
    if (index === -1) return undefined;
    
    this.items[index] = { ...this.items[index], ...updates };
    return this.items[index];
  }
  
  delete(id: number): boolean {
    const index = this.items.findIndex(item => item.id === id);
    if (index === -1) return false;
    
    this.items.splice(index, 1);
    return true;
  }
  
  count(): number {
    return this.items.length;
  }
}

// User entity
interface User extends Entity {
  name: string;
  email: string;
  age: number;
}

// Product entity
interface Product extends Entity {
  name: string;
  price: number;
  inStock: boolean;
}

// Create repositories
const userRepo = new Repository<User>();
const productRepo = new Repository<Product>();

// User operations
const user1 = userRepo.create({
  name: "Yusuf Ibrahim",
  email: "yusuf@example.com",
  age: 25
});

const user2 = userRepo.create({
  name: "Amina Hassan",
  email: "amina@example.com",
  age: 30
});

console.log(`Created user: ${user1.name}`);
console.log(`Total users: ${userRepo.count()}`);

userRepo.update(user1.id, { age: 26 });
const updatedUser = userRepo.findById(user1.id);
console.log(`Updated age: ${updatedUser?.age}`);

// Product operations
const product1 = productRepo.create({
  name: "Laptop",
  price: 450000,
  inStock: true
});

const product2 = productRepo.create({
  name: "Mouse",
  price: 5000,
  inStock: true
});

console.log(`Created product: ${product1.name}`);
console.log(`Total products: ${productRepo.count()}`);

const allProducts = productRepo.findAll();
allProducts.forEach(p => {
  console.log(`${p.name}: ₦${p.price}`);
});

Example 3: Event Emitter

event-emitter.ts
// Generic event emitter
class EventEmitter<T> {
  private listeners: Array<(data: T) => void> = [];
  
  on(listener: (data: T) => void): void {
    this.listeners.push(listener);
  }
  
  off(listener: (data: T) => void): void {
    const index = this.listeners.indexOf(listener);
    if (index !== -1) {
      this.listeners.splice(index, 1);
    }
  }
  
  emit(data: T): void {
    this.listeners.forEach(listener => listener(data));
  }
  
  once(listener: (data: T) => void): void {
    const onceWrapper = (data: T) => {
      listener(data);
      this.off(onceWrapper);
    };
    this.on(onceWrapper);
  }
}

// User login event
interface UserLoginEvent {
  userId: number;
  username: string;
  timestamp: Date;
}

// Order created event
interface OrderCreatedEvent {
  orderId: number;
  customerId: number;
  total: number;
}

// Create typed emitters
const loginEmitter = new EventEmitter<UserLoginEvent>();
const orderEmitter = new EventEmitter<OrderCreatedEvent>();

// User login listeners
loginEmitter.on((event) => {
  console.log(`User ${event.username} logged in at ${event.timestamp}`);
});

loginEmitter.on((event) => {
  console.log(`Logging user activity for user ${event.userId}`);
});

// Order created listeners
orderEmitter.on((event) => {
  console.log(`Order #${event.orderId} created for ₦${event.total}`);
});

orderEmitter.once((event) => {
  console.log(`First order notification: #${event.orderId}`);
});

// Emit events
loginEmitter.emit({
  userId: 1,
  username: "Yusuf",
  timestamp: new Date()
});

orderEmitter.emit({
  orderId: 1001,
  customerId: 1,
  total: 50000
});

orderEmitter.emit({
  orderId: 1002,
  customerId: 2,
  total: 75000
});
// First order notification only appears once

Test Your Knowledge

Type Safety Check

Which generic function is correctly defined?

// Option A
function identity(arg: any): any {
  return arg;
}

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

// Option C
function identity<T>(arg: any): T {
  return arg;
}

// Option D
function identity(arg: T): T {
  return arg;
}

Common TypeScript Error

Code with Error
// Function without generics - loses type information
function getFirstElement(arr: any[]): any {
  return arr[0];
}

const numbers = [1, 2, 3];
const first = getFirstElement(numbers);

// Lost type information!
// TypeScript thinks 'first' is 'any'
first.toFixed(2);        // No error but might crash
first.toUpperCase();     // No error but will crash
first.whatever();        // No error but will crash

❌ No compile-time error, but runtime crashes possible because type is 'any'

What's Wrong?

Without generics, functions that work with different types must use 'any', which loses all type safety. TypeScript can't help you use the result correctly.

Corrected Code
// Function with generics - preserves type information
function getFirstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const numbers = [1, 2, 3];
const first = getFirstElement(numbers);

// Type preserved! TypeScript knows 'first' is 'number | undefined'
if (first !== undefined) {
  first.toFixed(2);      // ✅ Works - TypeScript knows it's a number
  // first.toUpperCase(); // ❌ Error - TypeScript knows numbers don't have toUpperCase
}

const strings = ["hello", "world"];
const firstStr = getFirstElement(strings);

// Type is 'string | undefined'
if (firstStr !== undefined) {
  firstStr.toUpperCase();  // ✅ Works - TypeScript knows it's a string
  // firstStr.toFixed(2);  // ❌ Error - TypeScript knows strings don't have toFixed
}

Solution: Use generics to preserve type information

Best Practices

  1. Use meaningful type parameter names - T for single type, descriptive names for multiple
  2. Let TypeScript infer when possible - explicit types when needed
  3. Prefer generics over any - maintain type safety
  4. Use generics for reusable code - avoid duplication
  5. Keep generic functions simple - easy to understand
  6. Document complex generics with comments
  7. Consider default type parameters for flexibility
  8. Test with different types to ensure generics work
best-practices-example.ts
// ✅ Good: Let TypeScript infer
function wrap<T>(value: T): T[] {
  return [value];
}

const nums = wrap(5);        // Inferred as number[]
const strs = wrap("hello");  // Inferred as string[]

// ✅ Good: Explicit when needed
const explicit = wrap<number>(5);  // Explicitly number[]

// ✅ Good: Descriptive names for multiple parameters
function mapValues<TInput, TOutput>(
  items: TInput[],
  mapper: (item: TInput) => TOutput
): TOutput[] {
  return items.map(mapper);
}

// ✅ Good: Generic interface
interface Result<T> {
  success: boolean;
  data?: T;
  error?: string;
}

// ✅ Good: Generic class
class Cache<T> {
  private data = new Map<string, T>();
  
  set(key: string, value: T): void {
    this.data.set(key, value);
  }
  
  get(key: string): T | undefined {
    return this.data.get(key);
  }
}

// ❌ Avoid: Using 'any' instead of generics
function badWrap(value: any): any[] {
  return [value];  // Loses type information
}

// ❌ Avoid: Too complex generics
function confusing<T, U, V, W, X, Y, Z>(/* too many params */): any {
  // Hard to understand and maintain
}

// ✅ Better: Keep it simple
function simple<T>(value: T): T {
  return value;
}

Key Takeaways

  • Generics create reusable code that works with multiple types
  • Type parameters (T) act like function parameters for types
  • Generics preserve type information, unlike any
  • TypeScript can infer generic types from arguments
  • Generic functions, interfaces, and classes are all supported
  • Multiple type parameters enable complex generic patterns
  • Generics work with arrays, objects, and custom types
  • Use generics to avoid code duplication
  • Always prefer generics over any for type safety
  • Generics are fundamental to TypeScript's power

What's Next?

Excellent work! You've mastered the basics of generics and understand how to create reusable, type-safe code. Next, we'll explore Generic Constraints. You'll learn how to restrict generic types using extends, create bounded generics, and ensure your generic code works only with types that have specific properties. This gives you even more control over your generic types!

Get ready to master advanced generic patterns!

Mastering TypeScript generics! Check this out!

Previous
Type Assertions and Casting
Next
Generic Constraints

Never Miss a New TypeScript Tutorial

Join 1,000+ developers learning TypeScript step-by-step. Get new tutorials, type safety tips, and exclusive resources delivered to your inbox - completely FREE.

You might also like

UI Components

Ready-made components for your projects

Developer Tools

Boost productivity with handy generators

HTML Game

Learn HTML concepts through interactive gameplay

Join Our Web Development Training Program

Master frontend & backend development with hands-on projects.

TypeScript Tutorials

0 of 30 completed

Your Progress0%

Topics

Getting Started

  • What is TypeScript?
  • TypeScript vs JavaScript
  • Setting Up TypeScript
  • Your First TypeScript Program

Basic Types

  • Primitive Types
  • Arrays and Tuples
  • Objects and Type Aliases
  • Enums
  • Any, Unknown, and Never

Functions & Interfaces

  • Function Types and Signatures
  • Interfaces
  • Optional and Default Parameters
  • Function Overloading

Advanced Types

  • Union and Intersection Types
  • Type Guards and Narrowing
  • Literal Types
  • Type Assertions and Casting

Generics

  • Introduction to Generics
  • Generic Constraints
  • Utility Types

Classes & OOP

  • Classes and Constructors
  • Access Modifiers
  • Inheritance and Polymorphism
  • Abstract Classes and Interfaces

Practical TypeScript

  • Working with Modules
  • TypeScript with React
  • Async/Await and Promises
  • Working with APIs and JSON
  • Type Declarations and DefinitelyTyped
  • TypeScript Best Practices
Falytom logoFALYTOM

Learn technologies like HTML, CSS, JavaScript, React, NodeJS and more through interactive games, tutorials, components and free tools. Transform your businesses with AI chatbots, SEO-optimized websites, and professional development services.

© 2025 Tomilola Group
  • Facebook logo
  • Twitter logo
  • Instagram logo
  • LinkedIn logo
  • GitHub logo
  • YouTube logo