Why generics exist, starting from duplication
Generics are the feature that trips up more newcomers than any other — the <T> syntax looks alien,
and the purpose is not obvious. So this lesson does not start with syntax. It starts with a problem —
code duplication that throws away type information — and shows generics as the natural solution. Once
you see the problem they solve, the syntax stops being scary.
The problem: reuse versus type safety
Say you want a function that returns the first element of an array. For a string array:
function firstString(arr: string[]): string {
return arr[0];
}
Now you need it for numbers, so you copy it:
function firstNumber(arr: number[]): number {
return arr[0];
}
And for booleans, and for Users, and... you are writing the same function over and over, once per
type. That is exactly the duplication functions were invented to eliminate. So you try to write it
once — but how do you type it?
Attempt 1: any. Make it work for anything by giving up types:
function first(arr: any[]): any {
return arr[0];
}
const s = first(["Pune", "Mumbai"]); // s is `any` — we LOST the type!
s.toFixed(); // no error — but s is a string; this crashes at runtime
This is reusable but throws away the type. first(["Pune"]) returns any, so TypeScript no longer
knows the result is a string, and you get no safety — the very thing you came to TypeScript for. any
buys reuse by surrendering type information.
Attempt 2: a union. Enumerate the types:
function first(arr: (string | number)[]): string | number {
return arr[0];
}
Now it only works for strings and numbers (not booleans, not Users), and even then the result is
string | number — you have to narrow it every time, and it does not scale to new types.
Neither attempt is satisfying: any loses safety, unions do not scale and lose precision. What you
actually want is: write the function once, and have it return the specific type of whatever array
you pass. That is precisely what a generic does.
The solution: a type parameter
A generic introduces a type parameter — a placeholder for a type that is filled in when the function is called:
function first<T>(arr: T[]): T {
return arr[0];
}
Read <T> as "for some type T". The function takes a T[] and returns a T — whatever T turns
out to be. T is not a specific type; it is a variable that stands for a type, decided per call:
const s = first(["Pune", "Mumbai"]); // T is string -> s is string
const n = first([1, 2, 3]); // T is number -> n is number
const b = first([true, false]); // T is boolean -> b is boolean
s.toUpperCase(); // fine — s is string
n.toFixed(); // fine — n is number
One function, and it returns the exact type of the array you gave it. first(["Pune"]) returns a
string (not any, not string | number) — full type safety, and it works for every type,
including ones that do not exist yet. This is the generic's promise: reuse without losing type
information. You write the logic once, and the types flow through precisely.
T is a type variable, decided per call
The key mental shift: T is to types what a parameter is to values. A normal parameter is a
placeholder for a value, filled in when you call the function. A type parameter is a placeholder for a
type, filled in (usually inferred) when you call the function:
function identity<T>(value: T): T {
return value;
}
identity(42); // T inferred as number -> returns number
identity("Pune"); // T inferred as string -> returns string
identity<boolean>(true); // T explicitly boolean (usually unnecessary — inference gets it)
Notice TypeScript infers T from the argument you pass — you rarely write identity<number>(42)
explicitly, because it can see 42 is a number. This inference is what makes generics pleasant to
use: you call first(myArray) like any function, and the types just work, no <> needed at the call
site.
Where you have already used generics
Generics are not exotic — you have been using them since the arrays lesson:
const nums: Array<number> = [1, 2, 3]; // Array<number> — Array is generic
const items: number[] = [1, 2, 3]; // same thing, sugar for Array<number>
const result = [1, 2, 3].map(n => n * 2); // map is generic — returns T[] for the callback's return T
Promise.resolve("Pune"); // Promise<string> — Promise is generic
Array<T>, Promise<T>, Map<K, V>, and every collection method are generic — that is how
[1,2,3].map(n => ${n}) knows the result is string[]. So you already rely on generics constantly;
this module just teaches you to write your own, which you will do whenever you build reusable code —
a data structure, a utility, an API client (the capstone).
The one-sentence purpose
Whenever you catch yourself about to duplicate a function for different types, or reaching for any
because "it should work for anything", that is a generic. A generic lets you write code once that
works for many types while preserving the specific type at each use. That is the whole idea, and the
rest of the module is how to wield it — writing generic functions, constraining them, and generic
types.
Check your work
The problem generics solve. Writing the same function once per type (duplication) versus using
any (loses type safety) or a union (does not scale, loses precision).
Why any is unsatisfying for reuse. It makes the function reusable but throws away the type — the
result is any, so you get no safety.
What a type parameter is. A placeholder for a type, written <T>, filled in (usually inferred) when
the function is called — a type variable.
What function first<T>(arr: T[]): T does. Returns the exact element type of whatever array you
pass — one function, full safety, every type.
T versus a normal parameter. A normal parameter is a placeholder for a value; a type parameter is
a placeholder for a type.
How T is usually determined. Inferred from the argument — you rarely write <number> explicitly.
Where you have already used generics. Array<T>, Promise<T>, Map<K,V>, and map/filter —
all generic.
The one-sentence purpose. Write code once that works for many types while preserving the specific type at each use.
Practice
- Write
firstStringandfirstNumberseparately and feel the duplication. - Write
first(arr: any[]): anyand confirm the result isany— call a wrong method and note there is no error (lost safety). - Write the generic
first<T>(arr: T[]): T. Call it with a string array, a number array, and a boolean array; confirm each result has the exact type. - Call a string method on the result of
first(["Pune"])and a number method onfirst([1,2]); confirm each is allowed and the wrong one errors. - Write
identity<T>(value: T): Tand confirmTis inferred from the argument. - Hover over
[1,2,3].map(n =>${n})and confirm the result type isstring[]— notemapis generic. - Find a place in your code where you used
anyfor "it could be any type" and consider whether a generic would restore the safety.
Official documentation
- TypeScript — Generics — The full introduction.
- TypeScript — Generic functions — Type parameters on functions.
- TypeScript — Hello World of Generics — The
identityexample.
Next: writing generic functions, in depth.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship