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.

Related essays

Next essay
TypeScript Utility Types: Complete Reference Guide