Skip to content

Generics

Suppose you want a function that returns whatever you give it. Without generics you face a bad choice:

// Option A: one type only — not reusable
function identityStr(x: string): string { return x; }
// Option B: any — reusable, but throws away all type information
function identityAny(x: any): any { return x; }
const a = identityAny(42);
a.toUpperCase(); // 💥 no error at compile time — any hides the bug

any makes the function reusable but blinds the compiler: the link between the input type and the output type is gone. Generics keep that link.

// @expect-error TS2339
function identity<T>(x: T): T { return x; }
const n = identity(42); // T inferred as number → n: number
const s = identity("hi"); // T inferred as string → s: string
n.toUpperCase(); // ❌ Error: number has no toUpperCase — the bug is caught

T is a type parameter — a placeholder the caller (or inference) fills in. The function works for every type, and the return type still tracks the argument type exactly.

A generic is a function at the type level: it takes types as input and produces types as output. identity<T> says “for any type T, take a T and return a T.” This is called parametric polymorphism — one implementation, parameterized over types.

flowchart LR
  impl["one generic
identity of T"] --> callN["called with number
returns number"]
  impl --> callS["called with string
returns string"]
  impl --> callU["called with User
returns User"]
A generic is one implementation over many types
LessonWhat you’ll learn
Functions & constraintsType parameters, extends constraints, default type parameters
Classes & interfacesGeneric classes, interfaces, per-method type parameters, and variance
Conditional typesT extends U ? X : Y and distribution over unions
Inference & inferHow type arguments are inferred, and extracting types with infer
Variancein/out annotations, Promise/Map/Set/Array variance, and the AI-code smells variance produces
Section titled “The link only exists if the implementation keeps it”

Everything above sells generics on one promise: identity<T> returns exactly the type it received, unlike identityAny. That promise is a property of the signature, and the compiler enforces it at every call site — but nothing forces the implementation to actually honor it internally. A “generic” that appears in the signature purely to look safe, while the body quietly breaks the link with a cast, is no safer than identityAny was:

interface User { id: string; name: string }
// T appears ONLY in the return position — nothing links it to any argument,
// so it can never be inferred from a call. This "generic" is really `as T`
// with extra steps: the caller supplies T by hand, and the compiler trusts it.
function parseAs<T>(json: string): T {
return JSON.parse(json);
}
const user = parseAs<User>('{"id":"1"}'); // no "name" field at all — still compiles
console.log(user.name.toUpperCase());
// @run
interface User { id: string; name: string }
function parseAs<T>(json: string): T {
return JSON.parse(json);
}
const user = parseAs<User>('{"id":"1"}');
console.log(user.name.toUpperCase());

This compiles with zero errors — JSON.parse returns any, and any is assignable to anything, including the caller’s chosen T. Running it crashes:

TypeError: Cannot read properties of undefined (reading 'toUpperCase')

Compare that with a generic that keeps its promise, because its type parameter genuinely connects an argument to the result:

A real link between input and output

interface Box<T> { value: T }
// T appears once in the parameter list — but it ALSO appears in the return
// type, linking the argument to the result. That is a real generic link,
// not a decoration, even though T shows up only once among the parameters.
function box<T>(value: T): Box<T> {
return { value };
}
const b = box("hi");
// ^? Box<string>
console.log(b.value.toUpperCase()); // ✅ still string — the link survived
Open in TS Playground

The difference is not how many times T is written — it is whether T actually ties something the caller supplies to something the caller gets back. box’s T does that. parseAs’s T does not: it is a caller-supplied label glued onto a value the function never inspected, which is exactly what a cast is. The rest of this module is about building generics whose type parameters genuinely link positions — and about the two lessons ahead (classes, and conditional types + inference) that show exactly how far that link can be trusted.

flowchart TD
  sig["Signature: identity of T takes T returns T"] --> promise["Promise: whatever comes in, comes back out unchanged"]
  promise --> kept["Implementation actually uses the argument
(box, identity, first)"] --> real["Link is real — compiler-checked at every call"]
  promise --> broken["Implementation ignores the argument, casts, or parses untyped data
(parseAs, JSON.parse as T)"] --> fake["Link is fake — caller's T is trusted, never verified"]
A generic is a promise the signature makes and the implementation must keep

A generic signature is a contract, but TypeScript only checks that the contract’s shape is consistent — that every place T appears agrees with every other place. It does not, and cannot, check that the function’s body actually derives the output from the input the way the signature implies. identity<T>(x: T): T { return x; } keeps the promise because it is trivially forced to: there is nothing else to return. The moment a function’s body can produce a T-shaped value without deriving it from an argument — parsing JSON, reading any, casting — the signature becomes a claim floated on trust, identical in kind to the custom type guards from the narrowing lesson that the compiler trusts without checking their bodies.

ApproachBenefitCost
anySimplest to write, accepts everythingThrows away the input↔output link entirely; every use is silently unchecked
Generic (T linking real positions)Compiler enforces the link at every call site, for every concrete typeRequires designing a signature where T is actually inferable, not just declared
Generic used as a disguised cast (T only in output)Looks exactly like a safe generic in the signatureCannot be inferred, must be supplied by the caller, and is only as trustworthy as whatever the caller claims
Overload setDocuments a small, fixed set of exact input→output pairings individuallyDoesn’t scale past a handful of cases; see Generic Functions & Constraints for a case where it beats a generic
  • Any function with <T> in its signature is automatically type-safe. Only if T is actually derived from something the caller passes in. A T that appears only in the return position is filled in by the caller’s own annotation, with nothing to check it against — no different from an as cast.
  • A generic function is always better than one typed with any. Only if the generic’s type parameter genuinely links a position the caller controls to a position the caller reads back. Otherwise it’s any wearing a safer-looking signature.
  • Generics exist to make code “more type-safe” in the abstract. They exist for one specific reason: to preserve a link between input and output types that a single concrete type or any would lose. If there’s no link to preserve, a generic adds ceremony without adding safety.

Before trusting a generic function in generated code, find every position T (or any other type parameter) appears in, and ask whether at least one of those positions is a parameter the compiler can actually inspect at the call site:

  • A type parameter that appears only in the return type (function parseAs<T>(json: string): T) can never be inferred — it is supplied by the caller and trusted, exactly like a cast. This is one of the most common shapes in AI-generated data-fetching and parsing code.
  • A type parameter that appears in a parameter whose value is itself untyped or any (the result of JSON.parse, fetch().json(), a third-party callback typed any) gives you the appearance of a link with none of the safety — the “input” side was never actually checked either.
  • A generic wrapper that immediately casts its own parameter to any or unknown internally, then casts the result back to T, has the same problem as parseAs, just spread across two lines instead of one.

🐛 Spot the bug

interface Product { id: string; price: number }
function fetchTyped<T>(url: string): Promise<T> {
return fetch(url).then((r) => r.json());
}
async function run() {
const product = await fetchTyped<Product>("/api/product/1");
console.log(product.price.toFixed(2));
}
Show the answer

This compiles with zero errors and can still crash or silently misbehave at runtime. T appears only in the return position of fetchTyped — it is never derived from url or from anything the function actually inspects. .json() returns Promise<any>, and any is assignable to the caller’s chosen T with no verification at all. If the endpoint’s real response shape doesn’t match Product (a renamed field, a price sent as a string, a wrapped { data: ... } envelope), fetchTyped<Product> still “returns” a Product as far as the type system is concerned, and product.price.toFixed(2) either crashes or silently does the wrong thing depending on what actually came back. The fix is the same one from the Typing APIs & Boundaries lesson: validate the shape at the boundary (a schema library, or at minimum a runtime check) instead of asserting it through a generic type parameter that nothing ever checks against reality.

What do generics preserve that any throws away?
In function identity<T>(x: T): T, what is T?
What is the name for "one implementation parameterized over types"?
Does this compile, and why or why not?
interface User { id: string; name: string }

function parseAs<T>(json: string): T {
  return JSON.parse(json);
}

const user = parseAs<User>('{"id":"1"}');
console.log(user.name.toUpperCase());
Why does function box<T>(value: T): Box<T> count as a genuine generic link, even though T appears only once among the *parameters*?
A generic class or interface can also have a variance problem — assigning Stack<Dog> to a variable typed Stack<Animal>. Where in this module is that covered in full?