Skip to content
All essays
WebFebruary 9, 202514 min

TypeScript Generics: Complete Guide to Flexible Types

Master TypeScript generics. Learn generic functions, classes, constraints, and advanced generic patterns.

Ü
Ümit Uz
Mobile & Full Stack Developer

What are Generics?

Generics allow you to create reusable components that work with a variety of types while maintaining type safety.

Generic Functions

Basic Generic Function

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

const num = identity<number>(42);  // 42
const str = identity('hello');  // Type inference

Multiple Type Parameters

typescript
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const p1 = pair(1, 'one');  // [number, string]
const p2 = pair('hello', 42);  // [string, number]

Generic Constraints

typescript
// Constrain to specific properties
function logLength<T extends { length: number }>(arg: T): void {
  console.log(arg.length);
}

logLength('hello');  // OK - string has length
logLength([1, 2, 3]);  // OK - array has length
logLength(42);  // Error - number doesn't have length

// Constrain to specific type
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'John', age: 30 };
const name = getProperty(user, 'name');  // string
const age = getProperty(user, 'age');  // number
const invalid = getProperty(user, 'email');  // Error

Default Type Parameters

typescript
function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

const strings = createArray(3, 'hello');  // string[]
const numbers = createArray<number>(3, 42);  // number[]
const defaults = createArray(3, true);  // boolean[]

Generic Classes

Basic Generic Class

typescript
class Box<T> {
  private contents: T;

  constructor(value: T) {
    this.contents = value;
  }

  get(): T {
    return this.contents;
  }

  set(value: T): void {
    this.contents = value;
  }
}

const stringBox = new Box('hello');
const numBox = new Box(42);

Generic Class with Constraints

typescript
class NumberWrapper<T extends number> {
  constructor(public value: T) {}

  add(n: T): T {
    return (this.value + n) as T;
  }
}

const intWrapper = new NumberWrapper(10);
const floatWrapper = new NumberWrapper(3.14);

Generic Class with Multiple Types

typescript
class Pair<T, U> {
  constructor(
    public first: T,
    public second: U
  ) {}

  swap(): Pair<U, T> {
    return new Pair(this.second, this.first);
  }
}

const pair = new Pair(1, 'one');
const swapped = pair.swap();  // Pair<string, number>

Generic Interfaces

Basic Generic Interface

typescript
interface Repository<T> {
  findById(id: string): Promise<T | null>;
  findAll(): Promise<T[]>;
  create(entity: Omit<T, 'id'>): Promise<T>;
  update(id: string, updates: Partial<T>): Promise<T>;
  delete(id: string): Promise<void>;
}

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

class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> {
    // Implementation
    return null;
  }

  // ... other methods
}

Generic Interface with Constraints

typescript
interface Identifiable {
  id: string | number;
}

interface DataService<T extends Identifiable> {
  get(id: T['id']): Promise<T>;
  save(entity: T): Promise<void>;
}

class UserService implements DataService<User> {
  async get(id: string): Promise<User> {
    // Implementation
    return {} as User;
  }

  async save(user: User): Promise<void> {
    // Implementation
  }
}

Advanced Generic Patterns

Conditional Types

typescript
type NonNullable<T> = T extends null | undefined ? never : T;

type Flatten<T> = T extends any[] ? T[number] : T;

type Nested = number[][];
type Flat = Flatten<Nested>;  // number[]

Mapped Types with Generics

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

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

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

Utility Functions

typescript
// Generic mapper
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
  return array.map(fn);
}

const numbers = [1, 2, 3];
const doubled = map(numbers, n => n * 2);  // number[]
const strings = map(numbers, n => n.toString());  // string[]

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

const evens = filter(numbers, n => n % 2 === 0);  // number[]

Generic Pipeline

typescript
type Pipeline<T> = T extends []
  ? never
  : T extends [infer First]
    ? First
    : T extends [infer First, ...infer Rest]
      ? First extends (arg: any) => any
        ? Pipeline<Rest> extends never
          ? never
          : (arg: ReturnType<First>) => ReturnType<First>
        : never
      : never;

// Usage
const add = (n: number) => n + 1;
const double = (n: number) => n * 2;
const toString = (n: number) => n.toString();

type Result = Pipeline<[typeof add, typeof double, typeof toString]>;
// (arg: string) => string

Practical Examples

API Client

typescript
class ApiClient {
  async get<T>(url: string): Promise<T> {
    const response = await fetch(url);
    return response.json();
  }

  async post<T, U>(url: string, data: T): Promise<U> {
    const response = await fetch(url, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(data),
    });
    return response.json();
  }
}

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

interface Post {
  id: number;
  title: string;
}

const client = new ApiClient();

const user = await client.get<User>('/api/users/1');
const created = await client.post<User, Post>('/api/posts', {
  title: 'Hello',
});

State Manager

typescript
class Store<T> {
  private state: T;
  private listeners: Array<(state: T) => void> = [];

  constructor(initialState: T) {
    this.state = initialState;
  }

  getState(): T {
    return this.state;
  }

  setState(partial: Partial<T>): void {
    this.state = { ...this.state, ...partial };
    this.notify();
  }

  subscribe(listener: (state: T) => void): () => void {
    this.listeners.push(listener);
    return () => {
      this.listeners = this.listeners.filter(l => l !== listener);
    };
  }

  private notify(): void {
    this.listeners.forEach(listener => listener(this.state));
  }
}

interface AppState {
  user: User | null;
  loading: boolean;
}

const store = new Store<AppState>({
  user: null,
  loading: false,
});

store.subscribe((state) => {
  console.log('State changed:', state);
});

Generic Repository

typescript
abstract class BaseRepository<T extends { id: number }> {
  constructor(protected db: any) {}

  async findById(id: number): Promise<T | null> {
    const result = await this.db.query(
      'SELECT * FROM table WHERE id = ?',
      [id]
    );
    return result[0] || null;
  }

  async findAll(): Promise<T[]> {
    return this.db.query('SELECT * FROM table');
  }

  async create(entity: Omit<T, 'id'>): Promise<T> {
    const result = await this.db.insert(entity);
    return { ...entity, id: result.insertId };
  }

  async update(id: number, updates: Partial<T>): Promise<T> {
    await this.db.update(id, updates);
    return this.findById(id) as Promise<T>;
  }

  async delete(id: number): Promise<void> {
    await this.db.delete(id);
  }
}

class UserRepository extends BaseRepository<User> {
  async findByEmail(email: string): Promise<User | null> {
    const result = await this.db.query(
      'SELECT * FROM users WHERE email = ?',
      [email]
    );
    return result[0] || null;
  }
}

Best Practices

  1. 1Use descriptive type names: T, U, V for simple cases
  2. 2Constrain generics: Use extends for better type safety
  3. 3Provide defaults: For common use cases
  4. 4Document generics: Explain type parameters
  5. 5Prefer inference: Let TypeScript infer when possible
  6. 6Avoid overuse: Keep code readable

Conclusion

Generics enable flexible, reusable code while maintaining type safety. Master generics to write better TypeScript code.

Enjoyed this essay? Share it.

Related essays

Next essay
TypeScript Utility Types: Complete Reference Guide