Your First Generic Function
Intermediate · 24 min read · ▶ live playground · ✦ checkpoint
Generics are the steepest step in TypeScript — and the most misunderstood, because they're usually introduced as abstract algebra. They're not. A generic is a solution to a concrete, annoying problem you've probably already hit: a function that works for any type, without throwing the type away. This lesson takes the step slowly, and by the end you'll read <T> as calmly as you read a parameter name.
The duplication problem
Write a function that returns the first element of an array. Easy — for strings:
function firstString(arr: string[]): string {
return arr[0];
}
console.log(firstString(["ada", "grace", "linus"]));→ ada
Now you need it for numbers. Your options are all bad:
- Duplicate it —
firstNumber,firstBoolean,firstUser… one copy per type, forever. - Union it —
first(arr: (string | number)[]): string | number— now every caller gets back a "maybe either" they have to narrow, and the union still can't cover every future type. anyit —first(arr: any[]): any— it runs, but the return type isany: autocomplete dies, typos pass, and the checker is fired from the one place you did the most dynamic thing.
Look at what the three failures have in common: the function's logic never cared about the type, but every way of writing the signature forced a choice too early. What we want to say is: "this works for some type — whichever one you call it with — and returns that same type."
Type parameters — a variable for types
That sentence has a syntax. Add angle brackets after the function name, and inside them declare a type parameter — a placeholder that stands for "the type this particular call uses":
function first<T>(arr: T[]): T {
return arr[0];
}
const name = first(["ada", "grace", "linus"]); // T = string → name: string
const year = first([1969, 1981, 1998]); // T = number → year: number
console.log(name.toUpperCase()); // string methods: available
console.log(year.toFixed(0)); // number methods: available→ ADA / 1969 — one function, and each call site kept its own precise type.
Read the signature out loud — this is a skill worth drilling until it's automatic:
"
first, for some type T, takes an array of T and returns a T."
T is a variable that holds a type instead of a value. Regular parameters connect argument values to return values; type parameters connect argument types to return types. That link — "whatever type comes in is what goes out" — is the entire trick, and it's precisely the information that any throws away.
Try it — one function, every type
Run it, then use the > prompt: call pick with your own arrays — strings, booleans, even the objects from the last line — and watch the echo keep each call's type. Then try the break-it line to see the payoff: the checker knows what comes out because it saw what went in.
Where you've already seen this
Generics have been hiding in plain sight all course:
string[]is reallyArray<string>—Arrayis a generic type withT = string.arr.map(…)returns whatever your callback produces:mapis a generic method —map<U>(fn: (item: T) => U): U[]. Read it out loud: "for some type U, takes a function from T to U, returns an array of U." That's why inference trackednumber[] → stringso precisely back in 2.1.Promise<string>from lesson 1.3's glance — "a promise of a string" — is a generic type parameterized by what it resolves to.
You haven't been avoiding generics; you've been consuming them through inference since your first array. Today you learned to produce them. The rest of Section 7 adds the missing controls — constraining what T is allowed to be (extends), generic interfaces and classes, and the failure modes that make people say "my generics fight me" — but the mental model is already complete: a type parameter is how a function keeps its caller's type.
Checkpoint
Answer all three to mark this lesson complete