TechLead
Lesson 6 of 8
5 min read
TypeScript

Generics

Create reusable, type-safe components with generic types, constraints, and utility types

Introduction to Generics

Generics enable you to create reusable components that work with multiple types while maintaining type safety.

// Without generics - lose type information
function identityAny(arg: any): any {
  return arg;
}

// With generics - preserve type information
function identity<T>(arg: T): T {
  return arg;
}

// Usage
const num = identity<number>(42);     // explicit: number
const str = identity("hello");         // inferred: string

// Generic arrow function
const identityArrow = <T>(arg: T): T => arg;

Generic Interfaces and Types

// Generic interface
interface Box<T> {
  value: T;
  getValue(): T;
}

const numberBox: Box<number> = {
  value: 42,
  getValue() {
    return this.value;
  }
};

// Generic type alias
type Result<T> = {
  success: boolean;
  data: T;
  error?: string;
};

type UserResult = Result<User>;
type NumberResult = Result<number>;

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

const pair: KeyValuePair<string, number> = {
  key: "age",
  value: 30
};

Generic Constraints

// Constraint with extends
interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("hello");      // OK: string has length
logLength([1, 2, 3]);    // OK: array has length
// logLength(123);       // Error: number has no length

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

const person = { name: "Alice", age: 30 };
getProperty(person, "name");  // OK: "name" is a key
// getProperty(person, "email"); // Error: "email" is not a key

// Multiple constraints
interface Named {
  name: string;
}

interface Aged {
  age: number;
}

function greet<T extends Named & Aged>(entity: T): string {
  return `Hello ${entity.name}, you are ${entity.age} years old`;
}

Generic Classes

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

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

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

// Generic class with constraint
class Repository<T extends { id: number }> {
  private items: T[] = [];

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

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

  remove(id: number): void {
    this.items = this.items.filter(item => item.id !== id);
  }
}

Built-in Utility Types

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

// Partial - all properties optional
type PartialUser = Partial<User>;
const update: PartialUser = { name: "Alice" };

// Required - all properties required
type RequiredUser = Required<PartialUser>;

// Readonly - all properties readonly
type ReadonlyUser = Readonly<User>;
const user: ReadonlyUser = { id: 1, name: "Alice", email: "a@b.com", age: 30 };
// user.name = "Bob"; // Error!

// Pick - select specific properties
type UserPreview = Pick<User, "id" | "name">;

// Omit - exclude specific properties
type UserWithoutEmail = Omit<User, "email">;

// Record - create object type with keys and values
type UserRoles = Record<string, "admin" | "user" | "guest">;
const roles: UserRoles = {
  alice: "admin",
  bob: "user"
};

// Exclude - exclude types from union
type Status = "pending" | "active" | "completed" | "cancelled";
type ActiveStatus = Exclude<Status, "cancelled">;  // "pending" | "active" | "completed"

// Extract - extract types from union
type OnlyCompleted = Extract<Status, "completed" | "cancelled">;  // "completed" | "cancelled"

// NonNullable - remove null and undefined
type MaybeString = string | null | undefined;
type DefinitelyString = NonNullable<MaybeString>;  // string

// ReturnType - get function return type
function createUser() {
  return { id: 1, name: "Alice" };
}
type NewUser = ReturnType<typeof createUser>;  // { id: number; name: string }

// Parameters - get function parameter types
function greet(name: string, age: number): void {}
type GreetParams = Parameters<typeof greet>;  // [string, number]

Custom Utility Types

// Make specific properties optional
type PartialBy<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;

type UserWithOptionalEmail = PartialBy<User, "email">;

// Make specific properties required
type RequiredBy<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;

// Deep partial (recursive)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Nullable type
type Nullable<T> = T | null;

// Array element type
type ArrayElement<T> = T extends (infer U)[] ? U : never;
type Elem = ArrayElement<string[]>;  // string

// Promise unwrap
type Awaited<T> = T extends Promise<infer U> ? U : T;
type Result = Awaited<Promise<string>>;  // string

Practical Examples

// Generic API response
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
  const response = await fetch(url);
  const data = await response.json();
  return {
    data,
    status: response.status,
    message: "Success",
    timestamp: new Date()
  };
}

// Usage
const userResponse = await fetchData<User>("/api/user");
const usersResponse = await fetchData<User[]>("/api/users");

// Generic React component props
interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}

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

// Generic event handler
type EventHandler<T> = (event: T) => void;

const handleClick: EventHandler<React.MouseEvent> = (e) => {
  console.log(e.clientX, e.clientY);
};

Key Takeaways

  • • Generics create reusable, type-safe components
  • • Use extends to constrain generic types
  • • Utility types transform existing types
  • keyof gets keys of a type as union
  • • Conditional types enable type-level logic
  • • Create custom utility types for common patterns

Continue Learning