RizTech Academy logo
RizTech Academy
GenericsLesson 1 of 525 min

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

  1. Write firstString and firstNumber separately and feel the duplication.
  2. Write first(arr: any[]): any and confirm the result is any — call a wrong method and note there is no error (lost safety).
  3. 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.
  4. Call a string method on the result of first(["Pune"]) and a number method on first([1,2]); confirm each is allowed and the wrong one errors.
  5. Write identity<T>(value: T): T and confirm T is inferred from the argument.
  6. Hover over [1,2,3].map(n => ${n}) and confirm the result type is string[] — note map is generic.
  7. Find a place in your code where you used any for "it could be any type" and consider whether a generic would restore the safety.

Official documentation

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