TestForge | Aidevops | 📊 Plogger ✍️ Blog 📚 Docs
plogger

AI DevOps Korea

Turn AI service development and operations into one improvement loop

Aidevops.kr covers LLMOps, RAG, agents, observability, evaluation, and cost-performance optimization for production AI services.

TypeScript Generics: A Practical Guide

· Updated Apr 21
TypeScript Generics: A Practical Guide diagram
Visual guide to the key flow, architecture, and decision points covered in this post.
TypeScript generics are often introduced as a reuse tool, but their most important job in production code is preserving relationships between values. The question is rarely "Can this be generic?" It is "Does this generic design make the API contract clearer for the next engineer?"

That distinction matters because TypeScript can drift from helpful typing into type-level theater surprisingly fast.

What Generics Are Really For

Generics are most valuable when the caller and callee need to stay connected through the type system.

Common examples include:

  • data-fetching helpers that return the requested payload shape
  • repository interfaces that preserve entity types
  • form utilities that map field names to field values
  • event systems where payload type depends on event name

In each case, the generic parameter expresses a relationship, not just a placeholder.

When a Generic Is Better Than a Union

Teams often overuse generics for problems that would be simpler with:

  • unions
  • overloads
  • explicit named types
  • a duplicated function that is easier to read

A good rule is this: if the type parameter does not connect two or more positions meaningfully, it may not need to exist.

Example: A Useful Generic Contract

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; message: string };

async function requestJson<T>(url: string): Promise<ApiResult<T>> {
  const response = await fetch(url);

  if (!response.ok) {
    return { ok: false, message: response.statusText };
  }

  return { ok: true, data: (await response.json()) as T };
}

This is useful because the caller chooses T, and the function preserves that type through the result boundary.

Constraints Matter More Than Flexibility

Generic code becomes safer when constraints express what is required.

For example:

function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

The K extends keyof T constraint is doing the real work. Without it, the helper would be flexible but untrustworthy.

This is a core TypeScript lesson: well-chosen constraints improve both safety and editor experience.

Inference Is Part of the API

Developers often think only about whether a generic compiles. In practice, inference quality is part of the public API.

If callers constantly need manual annotations, type assertions, or workarounds, the generic is probably too clever.

Good generic APIs usually:

  • infer useful defaults automatically
  • keep type parameters few in number
  • produce readable errors
  • keep the mental model obvious from the call site

Conditional Types Need Restraint

Conditional types and infer are powerful, but they create a steep readability curve.

They are worth using when:

  • the utility solves a repeated boundary problem
  • the result type would otherwise be duplicated in many places
  • the team can still explain the behavior during code review

They are a poor fit when they exist mainly to impress or to avoid writing one extra explicit type.

Common Generic Anti-Patterns

  • adding <T> when a concrete type is clearer
  • introducing several type parameters with weak naming
  • making inference so complex that errors become unreadable
  • building one generic abstraction to cover unrelated cases
  • forcing runtime uncertainty to look precise through type assertions

The last point is especially dangerous. Types can describe trust, but they cannot create it.

Review Checklist

  • Does the generic preserve a meaningful type relationship?
  • Would a union, overload, or explicit type be clearer?
  • Are constraints expressing real rules instead of vague flexibility?
  • Is inference good enough that callers do not fight the API?
  • Can the team explain the design without turning code review into type archaeology?

Closing Judgment

Generics are excellent when they carry intent forward through a codebase. They are harmful when they turn ordinary APIs into puzzles. Strong TypeScript design is not about maximum abstraction. It is about preserving the right relationships while keeping the call site easy to understand.

Continue Reading

Related posts

Next Path

Keep exploring this topic as a system