1. javascript
  2. /typescript
  3. /generics

TypeScript Generics

Generics in TypeScript enable you to create reusable components that can work with multiple types while preserving type safety. They provide a way to make components flexible without sacrificing the benefits of strong typing, allowing you to write code that is both reusable and type-safe.

Generics are particularly powerful when creating libraries, utility functions, and data structures that need to work with various types while maintaining compile-time type checking.

Basic Generics

Generic functions and classes use type parameters to work with multiple types.

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

// Usage with explicit type
let stringResult = identity<string>("hello");
let numberResult = identity<number>(42);

// Usage with type inference
let inferredString = identity("world"); // TypeScript infers T as string
let inferredNumber = identity(123); // TypeScript infers T as number

// Generic function with multiple type parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

let stringNumberPair = pair("age", 25); // [string, number]
let booleanArrayPair = pair(true, [1, 2, 3]); // [boolean, number[]]

// Generic array function
function getFirstElement<T>(array: T[]): T | undefined {
  return array[0];
}

let firstString = getFirstElement(["a", "b", "c"]); // string | undefined
let firstNumber = getFirstElement([1, 2, 3]); // number | undefined

Generic Interfaces

Interfaces can use generic type parameters to define flexible contracts.

// Generic interface
interface Container<T> {
  value: T;
  getValue(): T;
  setValue(value: T): void;
}

// Implementing generic interface
class Box<T> implements Container<T> {
  constructor(private _value: T) {}

  get value(): T {
    return this._value;
  }

  getValue(): T {
    return this._value;
  }

  setValue(value: T): void {
    this._value = value;
  }
}

// Usage
let stringBox = new Box<string>("hello");
let numberBox = new Box<number>(42);

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

interface Dictionary<K, V> {
  add(key: K, value: V): void;
  get(key: K): V | undefined;
  has(key: K): boolean;
  remove(key: K): boolean;
  keys(): K[];
  values(): V[];
}

// Implementation
class SimpleDictionary<K, V> implements Dictionary<K, V> {
  private items: KeyValuePair<K, V>[] = [];

  add(key: K, value: V): void {
    this.items.push({ key, value });
  }

  get(key: K): V | undefined {
    const item = this.items.find(item => item.key === key);
    return item?.value;
  }

  has(key: K): boolean {
    return this.items.some(item => item.key === key);
  }

  remove(key: K): boolean {
    const index = this.items.findIndex(item => item.key === key);
    if (index !== -1) {
      this.items.splice(index, 1);
      return true;
    }
    return false;
  }

  keys(): K[] {
    return this.items.map(item => item.key);
  }

  values(): V[] {
    return this.items.map(item => item.value);
  }
}

Generic Constraints

Constraints limit generic types to specific shapes or capabilities.

// Basic constraint using extends
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // Now we can access .length
  return arg;
}

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

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

let person = { name: "Alice", age: 30, city: "New York" };
let name = getProperty(person, "name"); // string
let age = getProperty(person, "age"); // number
// let invalid = getProperty(person, "invalid"); // Error: invalid key

// Multiple constraints
interface Drawable {
  draw(): void;
}

interface Timestamped {
  timestamp: Date;
}

function processDrawableWithTime<T extends Drawable & Timestamped>(item: T): T {
  item.draw();
  console.log("Processed at:", item.timestamp);
  return item;
}

// Conditional constraints
function create<T extends string | number>(value: T): T extends string ? string[] : number[] {
  if (typeof value === "string") {
    return [value] as any;
  } else {
    return [value] as any;
  }
}

let stringArray = create("hello"); // string[]
let numberArray = create(42); // number[]

Generic Classes

Classes can be generic and work with different types while maintaining type safety.

// Generic class
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;
  }

  clear(): void {
    this.items = [];
  }

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

// Usage
let stringStack = new Stack<string>();
stringStack.push("first");
stringStack.push("second");
console.log(stringStack.pop()); // "second"

let numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
console.log(numberStack.peek()); // 2

// Generic class with constraints
abstract class Repository<T extends { id: string | number }> {
  protected items: T[] = [];

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

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

  remove(id: string | number): boolean {
    const index = this.items.findIndex(item => item.id === id);
    if (index !== -1) {
      this.items.splice(index, 1);
      return true;
    }
    return false;
  }

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

  abstract validate(item: T): boolean;
}

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

class UserRepository extends Repository<User> {
  validate(user: User): boolean {
    return user.name.length > 0 && user.email.includes("@");
  }

  findByEmail(email: string): User | undefined {
    return this.items.find(user => user.email === email);
  }
}

Advanced Generic Patterns

Complex generic patterns for advanced type manipulation.

// Mapped types with generics
type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Required<T> = {
  [P in keyof T]-?: T[P];
};

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

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

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// Recursive generic types
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};

// Generic utility for nested object manipulation
type NestedKeyOf<ObjectType extends object> = {
  [Key in keyof ObjectType & (string | number)]: ObjectType[Key] extends object
    ? `${Key}` | `${Key}.${NestedKeyOf<ObjectType[Key]>}`
    : `${Key}`;
}[keyof ObjectType & (string | number)];

// Function overloading with generics
function map<T, U>(array: T[], fn: (item: T) => U): U[];
function map<T, U>(array: T[], fn: (item: T, index: number) => U): U[];
function map<T, U>(array: T[], fn: (item: T, index?: number) => U): U[] {
  return array.map(fn);
}

// Generic type guards
function isArray<T>(value: T | T[]): value is T[] {
  return Array.isArray(value);
}

function hasProperty<K extends string>(
  obj: object,
  key: K
): obj is Record<K, unknown> {
  return key in obj;
}

// Example usage
interface Person {
  name: string;
  address: {
    street: string;
    city: string;
    country: string;
  };
}

type PersonKeys = NestedKeyOf<Person>; 
// "name" | "address" | "address.street" | "address.city" | "address.country"

const person: DeepReadonly<Person> = {
  name: "John",
  address: {
    street: "123 Main St",
    city: "Boston",
    country: "USA"
  }
};

// person.name = "Jane"; // Error: readonly
// person.address.city = "NYC"; // Error: readonly

Generic Utility Functions

Common patterns for creating reusable generic utilities.

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

function groupBy<T, K extends string | number>(
  array: T[],
  keyFn: (item: T) => K
): Record<K, T[]> {
  return array.reduce((groups, item) => {
    const key = keyFn(item);
    if (!groups[key]) {
      groups[key] = [];
    }
    groups[key].push(item);
    return groups;
  }, {} as Record<K, T[]>);
}

function unique<T>(array: T[], keyFn?: (item: T) => any): T[] {
  if (!keyFn) {
    return [...new Set(array)];
  }
  
  const seen = new Set();
  return array.filter(item => {
    const key = keyFn(item);
    if (seen.has(key)) {
      return false;
    }
    seen.add(key);
    return true;
  });
}

// Object utilities
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => {
    if (key in obj) {
      result[key] = obj[key];
    }
  });
  return result;
}

function omit<T, K extends keyof T>(obj: T, keys: K[]): Omit<T, K> {
  const result = { ...obj };
  keys.forEach(key => {
    delete result[key];
  });
  return result;
}

// Promise utilities
async function asyncMap<T, U>(
  array: T[],
  asyncFn: (item: T) => Promise<U>
): Promise<U[]> {
  return Promise.all(array.map(asyncFn));
}

async function asyncFilter<T>(
  array: T[],
  asyncPredicate: (item: T) => Promise<boolean>
): Promise<T[]> {
  const results = await Promise.all(array.map(asyncPredicate));
  return array.filter((_, index) => results[index]);
}

// Usage examples
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9];
const chunked = chunk(numbers, 3); // [[1,2,3], [4,5,6], [7,8,9]]

const users = [
  { name: "Alice", department: "Engineering" },
  { name: "Bob", department: "Marketing" },
  { name: "Charlie", department: "Engineering" }
];

const grouped = groupBy(users, user => user.department);
// { Engineering: [Alice, Charlie], Marketing: [Bob] }

const uniqueUsers = unique(users, user => user.department);
// [Alice, Bob] (one from each department)

Generic Error Handling

Generic patterns for robust error handling and result types.

// Result type for error handling
type Result<T, E = Error> = 
  | { success: true; data: T }
  | { success: false; error: E };

// Generic async operation wrapper
async function tryAsync<T>(
  operation: () => Promise<T>
): Promise<Result<T>> {
  try {
    const data = await operation();
    return { success: true, data };
  } catch (error) {
    return { 
      success: false, 
      error: error instanceof Error ? error : new Error(String(error))
    };
  }
}

// Option type for nullable values
type Option<T> = 
  | { type: "some"; value: T }
  | { type: "none" };

function some<T>(value: T): Option<T> {
  return { type: "some", value };
}

function none<T>(): Option<T> {
  return { type: "none" };
}

function mapOption<T, U>(option: Option<T>, fn: (value: T) => U): Option<U> {
  return option.type === "some" ? some(fn(option.value)) : none();
}

// Usage
async function fetchUserData(id: number): Promise<Result<User>> {
  return tryAsync(async () => {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`Failed to fetch user: ${response.status}`);
    }
    return response.json();
  });
}

const userResult = await fetchUserData(1);
if (userResult.success) {
  console.log(userResult.data.name); // Safe to access
} else {
  console.error(userResult.error.message);
}

Generics in TypeScript provide a powerful way to create flexible, reusable, and type-safe code. They enable you to write functions, classes, and interfaces that work with multiple types while preserving compile-time type checking and providing excellent IntelliSense support.