DEV Community

Atlas Whoff
Atlas Whoff

Posted on

TypeScript Generics: From Confused to Confident

Why Generics Feel Hard

Generics look like math. <T>, <T extends K>, <T, U>—it reads like a type system paper, not application code.

But the concept is simple: write code that works with any type, while keeping type safety.

The Problem Generics Solve

Without generics:

function firstElement(arr: number[]): number {
  return arr[0];
}

function firstString(arr: string[]): string {
  return arr[0];
}

// Duplicate code for every type... or
function first(arr: any[]): any {
  return arr[0]; // loses all type safety
}
Enter fullscreen mode Exit fullscreen mode

With generics:

function first<T>(arr: T[]): T {
  return arr[0];
}

const num = first([1, 2, 3]);    // TypeScript knows: number
const str = first(['a', 'b']);   // TypeScript knows: string
const user = first(users);       // TypeScript knows: User
Enter fullscreen mode Exit fullscreen mode

One function, full type safety, any type.

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

const p = pair('hello', 42); // [string, number]

// Map with type inference
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

const lengths = map(['hello', 'world'], s => s.length); // number[]
Enter fullscreen mode Exit fullscreen mode

Generic Constraints

Limit what types T can be:

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

longest('hello', 'hi');      // works — strings have .length
longest([1, 2, 3], [1, 2]); // works — arrays have .length
longest(10, 20);             // Error: number has no .length

// 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: 'Alice', age: 30 };
const name = getProperty(user, 'name'); // string
const age = getProperty(user, 'age');   // number
getProperty(user, 'email');             // Error: not a key of user
Enter fullscreen mode Exit fullscreen mode

Generic Interfaces and Types

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

// Implement for specific types
class UserRepository implements Repository<User> {
  async findById(id: string): Promise<User | null> {
    return db.users.findUnique({ where: { id } });
  }
  // ...
}

// Generic type aliases
type Result<T> = 
  | { success: true; data: T }
  | { success: false; error: string };

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

// Usage with type narrowing
const result = await fetchUser('123');
if (result.success) {
  console.log(result.data.email); // TypeScript knows data is User
} else {
  console.log(result.error); // TypeScript knows error is string
}
Enter fullscreen mode Exit fullscreen mode

Generic React Components

// Generic select component
interface SelectProps<T> {
  options: T[];
  value: T | null;
  onChange: (value: T) => void;
  getLabel: (option: T) => string;
  getValue: (option: T) => string;
}

function Select<T>({
  options,
  value,
  onChange,
  getLabel,
  getValue,
}: SelectProps<T>) {
  return (
    <select
      value={value ? getValue(value) : ''}
      onChange={(e) => {
        const selected = options.find(o => getValue(o) === e.target.value);
        if (selected) onChange(selected);
      }}
    >
      {options.map((option) => (
        <option key={getValue(option)} value={getValue(option)}>
          {getLabel(option)}
        </option>
      ))}
    </select>
  );
}

// Usage — fully typed
<Select<User>
  options={users}
  value={selectedUser}
  onChange={setSelectedUser}
  getLabel={(u) => u.name}
  getValue={(u) => u.id}
/>
Enter fullscreen mode Exit fullscreen mode

Utility Types (Built-in Generics)

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

type PublicUser = Omit<User, 'password'>;
// { id, name, email }

type UserUpdate = Partial<Pick<User, 'name' | 'email'>>;
// { name?: string; email?: string }

type ReadonlyUser = Readonly<User>;
// All fields readonly

type UserRecord = Record<string, User>;
// { [key: string]: User }

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

type UserFromPromise = Awaited<Promise<User>>; // User
Enter fullscreen mode Exit fullscreen mode

The mental model: generics are function parameters for your types. function<T> is "this function takes a type parameter T." Once that clicks, generics become readable.


TypeScript patterns, generics, and type utilities baked into a production codebase: Whoff Agents AI SaaS Starter Kit.

Top comments (0)