What Generics Are and Why They Matter
At the end of Module 04, fetchJson returned Promise<unknown>:
async function fetchJson(url: string): Promise<unknown> { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json();}The caller had to cast the result:
const data = await fetchJson(url) as ApiResponse;That cast is a lie TypeScript accepts without question. If the API returns a different shape — or if url is wrong — data is typed as ApiResponse even though the actual value is something else entirely. TypeScript trusted the cast and stopped checking.
This is the problem generics solve.
The approaches that do not work
Section titled “The approaches that do not work”Return any. Total loss of type checking downstream. Any method call on the result is accepted, including ones that crash at runtime.
Return unknown. Safe — TypeScript forces you to narrow before use — but requires a cast at every call site. The cast is unverified.
Write one function per type. fetchApiResponse, fetchHighScore, fetchQuestions — all identical except the return type. Duplication at scale.
Use overloads. More expressive than duplication, but still requires enumerating every possible return type. Does not generalize.
None of these give you a single function that is both reusable and fully typed.
What generics do
Section titled “What generics do”A generic function introduces a type parameter — a placeholder for a type the caller supplies:
async function fetchJson<T>(url: string): Promise<T> { const response = await fetch(url); if (!response.ok) throw new Error(`HTTP ${response.status}`); return response.json() as Promise<T>;}T is a type parameter. When you call fetchJson, you provide the type:
const data = await fetchJson<ApiResponse>(url);// data is ApiResponse — fully typed, no cast at the call siteTypeScript substitutes ApiResponse for T throughout the function’s signature. The return type becomes Promise<ApiResponse>, and data is typed as ApiResponse — not because of a cast, but because the type flows through.
The analogy
Section titled “The analogy”Think of a generic type parameter the same way you think of a function parameter — but for types rather than values.
A regular function takes a value and uses it in the body. A generic function takes a type and uses it in the signature and body. Just as you call buildUrl(10, 'easy') with specific values, you call fetchJson<ApiResponse>(url) with a specific type.
Where you have already used generics
Section titled “Where you have already used generics”You have been using generics throughout the course without noticing the syntax:
const questions: TriviaQuestion[] = []; // Array<TriviaQuestion>const promise: Promise<TriviaQuestion[]> = fetchQuestions(10);Array<T> and Promise<T> are generic types built into TypeScript’s standard library. TriviaQuestion[] is shorthand for Array<TriviaQuestion> — T is filled in as TriviaQuestion. Promise<TriviaQuestion[]> fills T with TriviaQuestion[].
Every time you wrote a typed array or a typed promise, you were supplying type arguments to generic types.
Exercise
Section titled “Exercise”Before writing any code, answer these questions:
- What is wrong with returning
unknownfromfetchJsonand casting at the call site? - Why can you not solve the
fetchJsonproblem by writing separate functions for each return type? Array<string>is a generic type. What is the type parameter? What does it represent?- What would
fetchJson<HighScore>(url)return? What wouldfetchJson<number>(url)return?
- Generics solve the problem of writing reusable code that is still fully typed.
- A type parameter is a placeholder — the caller provides the actual type at the call site.
- The alternatives —
any,unknownwith casts, per-type duplicates — all have unacceptable trade-offs. Array<T>andPromise<T>are generic types you have been using since Module 01.- Generics carry types through functions without unsafe casts.