DEV Community

Atlas Whoff
Atlas Whoff

Posted on

TypeScript Generics: Write Once, Use Everywhere

TypeScript Generics: Write Once, Use Everywhere

Generics let you write functions and types that work across many types without sacrificing type safety. Here's everything you need.

The Problem Generics Solve

// Without generics: duplicate code or lose types
function firstString(arr: string[]): string { return arr[0]; }
function firstNumber(arr: number[]): number { return arr[0]; }

// Or use any — but lose type safety
function first(arr: any[]): any { return arr[0]; }

// With generics: one function, full type safety
function first<T>(arr: T[]): T { return arr[0]; }

const s = first(['a', 'b', 'c']); // type: string
const n = first([1, 2, 3]);        // type: number
Enter fullscreen mode Exit fullscreen mode

Generic Functions

// Identity function
function identity<T>(value: T): T {
  return value;
}

// Pair
function pair<A, B>(a: A, b: B): [A, B] {
  return [a, b];
}

// Filter with type preservation
function filterDefined<T>(arr: (T | null | undefined)[]): T[] {
  return arr.filter((x): x is T => x != null);
}

const results = filterDefined([1, null, 3, undefined, 5]);
// type: number[]
Enter fullscreen mode Exit fullscreen mode

Generic Interfaces and Types

// API response wrapper
interface ApiResponse<T> {
  data: T;
  error: string | null;
  timestamp: number;
}

type UserResponse = ApiResponse<User>;
type PostsResponse = ApiResponse<Post[]>;

// Result type (like Rust's Result)
type Result<T, E = Error> =
  | { success: true; data: T }
  | { success: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const user = await db.user.findUnique({ where: { id } });
    if (!user) return { success: false, error: new Error('Not found') };
    return { success: true, data: user };
  } catch (e) {
    return { success: false, error: e as Error };
  }
}
Enter fullscreen mode Exit fullscreen mode

Constraints

// T must have a length property
function longest<T extends { length: number }>(a: T, b: T): T {
  return a.length >= b.length ? a : b;
}

// T must be a key of U
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { name: 'Atlas', age: 1 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age');   // number
// getProperty(user, 'foo')             // Error: 'foo' not in type
Enter fullscreen mode Exit fullscreen mode

Generic Classes

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.at(-1); }
  get size(): number { return this.items.length; }
}

const stack = new Stack<number>();
stack.push(1);
stack.push(2);
const top = stack.peek(); // type: number | undefined
Enter fullscreen mode Exit fullscreen mode

Conditional Types

// Unwrap Promise
type Awaited<T> = T extends Promise<infer U> ? U : T;

type A = Awaited<Promise<string>>;    // string
type B = Awaited<number>;              // number

// Flatten array one level
type Flatten<T> = T extends Array<infer Item> ? Item : T;

type C = Flatten<string[]>;   // string
type D = Flatten<number>;     // number
Enter fullscreen mode Exit fullscreen mode

Utility Types (Built-in Generics)

type User = { id: string; name: string; email: string; password: string };

type PublicUser = Omit<User, 'password'>;           // Remove password
type PartialUser = Partial<User>;                   // All optional
type RequiredUser = Required<PartialUser>;          // All required
type ReadonlyUser = Readonly<User>;                 // No mutations
type UserKeys = keyof User;                         // 'id' | 'name' | 'email' | 'password'
type UserPreview = Pick<User, 'id' | 'name'>;      // Only id and name
Enter fullscreen mode Exit fullscreen mode

TypeScript generics are used throughout the AI SaaS Starter Kit — typed API responses, generic form hooks, and Result types. $99 at whoffagents.com.

Top comments (0)