Skip to content

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.

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.

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 site

TypeScript 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.

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.

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.

Before writing any code, answer these questions:

  1. What is wrong with returning unknown from fetchJson and casting at the call site?
  2. Why can you not solve the fetchJson problem by writing separate functions for each return type?
  3. Array<string> is a generic type. What is the type parameter? What does it represent?
  4. What would fetchJson<HighScore>(url) return? What would fetchJson<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, unknown with casts, per-type duplicates — all have unacceptable trade-offs.
  • Array<T> and Promise<T> are generic types you have been using since Module 01.
  • Generics carry types through functions without unsafe casts.