Programming Guides

TypeScript Generics, Explained Without the Jargon

Generics look intimidating because of the angle brackets. Underneath, they are a simple idea: 'a placeholder for a type I will tell you later.'

L. Marín10 min read

Generics intimidate new TypeScript developers more than any other feature. The angle brackets, the <T,> syntax, the compiler errors that span six lines — none of it helps. But the underlying idea is genuinely simple, and once it clicks, most of the syntax stops being scary.

Here is the idea, in one sentence: a generic is a placeholder for a type you promise to fill in later.

The problem generics solve

Suppose you want a function that returns the first element of an array. A first attempt:

TypeScript
function first(arr: any[]): any {
  return arr[0];
}

This works, but it lies. The return type any throws away everything TypeScript knows. If you pass in an array of strings, you get back any, and the compiler will happily let you call .toFixed() on it.

We want to say: "whatever type is in the array, that same type comes out." That is exactly a placeholder.

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

Read <T> as: "there is some type I will call T; I do not know what it is yet." When you call the function, TypeScript fills T in:

TypeScript
first([1, 2, 3]);        // T is number, returns number
first(["a", "b"]);       // T is string, returns string

The caller decides T, and the function's signature stays honest.

The constraint that makes generics useful

Unconstrained generics are powerful but loose. Often you want to say: "T can be anything, but it must have a certain property." Enter extends:

TypeScript
function getLength<T extends { length: number }>(x: T): number {
  return x.length;
}

getLength("hello");    // OK — strings have .length
getLength([1, 2, 3]);  // OK — arrays have .length
getLength(42);         // Error — numbers have no .length

extends here does not mean inheritance in the OOP sense. It means "must be assignable to." T is still chosen by the caller, but only from types that have a length.

Two generics at once

Real signatures often relate two types. A map-like helper:

TypeScript
function transform<T, U>(arr: T[], fn: (item: T) => U): U[] {
  return arr.map(fn);
}

transform([1, 2, 3], n => n.toString());  // T=number, U=string -> string[]

The relationship "the function takes T and returns U, and the output array is U[]" is enforced by the signature. Get any of it wrong and the compiler tells you, before runtime.

The one trap to avoid

Generics that appear only in the return type defeat inference. This is the classic "no, I really want you to infer" mistake:

TypeScript
function parse<T>(json: string): T {
  return JSON.parse(json);
}
const x = parse<{ id: string }>("..."); // you must specify T

Here T is not used in any parameter, so TypeScript cannot infer it — you must supply it explicitly, and the cast inside parse is unchecked. This is not generics being broken; it is generics honestly telling you "I have no information to fill this in." When you see a generic that only appears in the return type, treat it as a cast with extra steps.

Takeaways

  • A generic is a placeholder the caller fills in.
  • extends constrains the placeholder, it does not mean subclassing.
  • Relate multiple generics to express real invariants.
  • If a generic only appears in the return type, it is a cast — use it knowingly.

Generics are not advanced. They are variables, for types. Once you read them that way, the angle brackets become punctuation, not a warning sign.

Related briefs