Skip to content
The Switch · JavaScript to TypeScript · Lesson 7

Generics, just enough

A courier who delivers any parcel without opening it.

Contents
Part 1 of 7 · The hook

The courier who never peeks

One function, any parcel, nothing lost on the way.

Think of a courier. You hand over a parcel, and they deliver it. They don't open it. They don't care whether it is sweets or a phone. The courier works for every parcel.

But here is the clever part. When the parcel arrives, the receiver knows what is in it. If you sent laddoos, laddoos arrive. The courier never turned them into something else.

Now think about how you would write that as a function. It takes a thing and gives the same thing back. In JavaScript that is easy, because JavaScript doesn't track what is inside. In TypeScript there is a problem. If you label the function "takes a number", it can't carry sweets. If you label it "takes anything", the receiver no longer knows what arrived.

Generics solve this. A generic function has a type parameter: a stand-in name for "whatever kind you give me". By convention it is called T, short for "type". The function says "I take a T and give back that same T". Then each call fills in the blank.

You have already met generics. string[] is "a list of string", and Array<string> is the same thing written the long way. This lesson shows how to write your own, and stops there. Generics can get deep, and you need only a little of them.

Remember: a generic is a function or type with a blank for the kind. The blank, called a type parameter, is filled in from what you pass.

Part 2 of 7 · In JavaScript

Anything in, who knows what out

JavaScript carries the parcel, and forgets the label.

A JavaScript courier is one line.

JavaScriptOpen in TypeScript Playground (opens in a new tab)
function deliver(parcel) {  return parcel;}const sweets = deliver("laddoo");const count = deliver(42);console.log(sweets.toUpperCase());console.log(count + 1);
Output
LADDOO
43

It works for text and for numbers. Nothing stops it, and nothing tells the receiver what came out. That matters with a function like first, which hands back the first item of a list.

JavaScriptOpen in TypeScript Playground (opens in a new tab)
function first(items) {  return items[0];}const firstScore = first([90, 85, 72]);const firstName = first(["Asha", "Ravi"]);console.log(firstScore);console.log(firstScore.toUpperCase());
Output
90
TypeError: firstScore.toUpperCase is not a function

The first score is a number, and we asked it to shout. JavaScript didn't know the list held numbers, so it didn't warn us. It found out when the line ran.

Remember: a plain JavaScript function works for every kind of input, but its output is a mystery until the code runs.

Part 3 of 7 · In TypeScript

One function, every kind

Write the blank once. Each call fills it in.

First, the cheap way out. You could label the courier any, which switches the checker off for that value.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
function deliverAny(parcel: any): any {  return parcel;}const count = deliverAny(42);// No error here, and none for count.toUpperCase() either: any switches the checker off.console.log(count + 1);

It compiles, and it also compiles when you call count.toUpperCase() on a number. The parcel goes in with a label and comes out with none. That is the problem generics solve.

Here is the courier with a type parameter. The <T> after the name declares the blank. Then parcel: T says the input is a T, and : T after the brackets says the output is the same T.

function deliver(parcel) {  return parcel;}const sweets = deliver("laddoo");const count = deliver(42);console.log(sweets.toUpperCase());console.log(count + 1);

Tinted lines are the ones that differ

TypeScript fills in T for you from what you pass. deliver("laddoo") gives back text, so sweets.toUpperCase() is allowed. deliver(42) gives back a number, so count + 1 is fine. You never wrote <string> or <number>. The checker worked it out, which is called inference (guessing the type from the code), the same idea as lesson 2.

Now first. The input is a list of T, written T[], and the output is a T or undefined, because an empty list has no first item.

function first(items) {  return items[0];}const firstScore = first([90, 85, 72]);const firstName = first(["Asha", "Ravi"]);console.log(firstScore);console.log(firstScore.toUpperCase());

Tinted lines are the ones that differ

Because first is generic, the number list gives back number | undefined. The checker knows what came out. The wrong call is refused where you typed it, which is what JavaScript couldn't do.

Remember: <T> declares a blank, and T in the parameters and the result links them: whatever goes in is what comes out.

Part 4 of 7 · How it works

Four things you need

Several blanks, constraints, keyof, and generic types.

1. More than one blank. Use as many type parameters as you need, with different names. Each one is filled in separately.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
function pair<A, B>(left: A, right: B): [A, B] {  return [left, right];}const entry = pair("Asha", 21);console.log(entry);
Output
[ 'Asha', 21 ]

entry has the type [string, number]. The first slot is text, the second a number, and they never get mixed up.

You can also give the blank yourself, in angle brackets, if inference can't see enough. An empty list has nothing to learn from, so first<string>([]) says "this is a list of text".

TypeScriptOpen in TypeScript Playground (opens in a new tab)
function first<T>(items: T[]): T | undefined {  return items[0];}// Normally TypeScript works T out. You can also say it yourself.const empty = first<string>([]);console.log(empty);
Output
undefined

2. Constraints with extends. Sometimes "any type" is too loose. A function that compares lengths needs a type that has a length. A constraint limits the blank: T extends { length: number } means "any T, as long as it has a numeric length". Text has one. So do lists.

function longest(a, b) {  return a.length >= b.length ? a : b;}console.log(longest("chai", "coffee"));console.log(longest(10, 20)); // numbers have no length, so this quietly picks 20

Tinted lines are the ones that differ

In JavaScript, longest(10, 20) quietly gives 20, because numbers have no length and both sides compare as undefined. TypeScript refuses the call, and says why. A string and a list both pass, because both have a length.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
function longest<T extends { length: number }>(a: T, b: T): T {  return a.length >= b.length ? a : b;}console.log(longest("chai", "coffee"));console.log(longest([1, 2, 3], [4, 5]));
Output
coffee
[ 1, 2, 3 ]

Notice that longest gives back the same type it was given. Pass two lists, get a list. Pass two texts, get text.

3. Keys of an object. A common trick reads a property by name. keyof T means "the names of the properties of T, as a union of exact words". So K extends keyof T says "the key must be one that this object really has", and T[K] is the type of that property.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type User = { name: string; age: number };function getField<T, K extends keyof T>(item: T, key: K): T[K] {  return item[key];}const user: User = { name: "Asha", age: 21 };console.log(getField(user, "name"));console.log(getField(user, "age"));
Output
Asha
21

getField(user, "name") is a string and getField(user, "age") is a number. A misspelled key is caught.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type User = { name: string; age: number };function getField<T, K extends keyof T>(item: T, key: K): T[K] {  return item[key];}const user: User = { name: "Asha", age: 21 };console.log(getField(user, "agee"));

TypeScript says

error TS2345: Argument of type '"agee"' is not assignable to parameter of type 'keyof User'.

4. Generic types. Types can have blanks too. A Box<T> is a box that holds a T. It is the parcel from our picture.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Box<T> = { value: T };function openBox<T>(box: Box<T>): T {  return box.value;}const luckyNumber: Box<number> = { value: 7 };const note: Box<string> = { value: "Call Amma" };console.log(openBox(luckyNumber) + 1);console.log(openBox(note).toUpperCase());
Output
8
CALL AMMA

You fill in the blank when you use the type, in angle brackets: Box<number>, Box<string>. Put the wrong thing inside and the checker says so.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Box<T> = { value: T };const luckyNumber: Box<number> = { value: "seven" };

TypeScript says

error TS2322: Type 'string' is not assignable to type 'number'.

You have been using these already. Array<number>, Map<string, number> and Promise<string> are all generic types from the language itself.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
const scores: Array<number> = [90, 85];const ages = new Map<string, number>();ages.set("Asha", 21);const later: Promise<string> = Promise.resolve("done");

A type parameter can also have a default, which is used when you leave the blank empty. Result<T = string> means a plain Result is a Result<string>.

TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Result<T = string> = { ok: boolean; value: T };const text: Result = { ok: true, value: "saved" };const count: Result<number> = { ok: true, value: 3 };

Remember: several blanks are fine, extends limits a blank, keyof names an object's properties, and Box<T> is a type with a blank.

Part 5 of 7 · Watch out

Don't over-generic

Four ways generics go wrong.

A blank used once is not doing anything. If T appears only once in a function's signature, it links nothing. function log<T>(value: T): void can be written function log(value: unknown): void. A blank earns its place by connecting two things: input to output, or one parameter to another.

Generics are not a way to avoid any, or a way to hide it. Writing <T> and then casting value as T inside the function is the same as any with extra steps. The checker trusted you.

Names are only names. T is a habit. Longer names like Item or Key are fine, and clearer once you have several blanks.

Start without generics. If you only ever call the function with one type, write it for that type. Add a blank when you find yourself copying a function for a second type. Most day-to-day TypeScript uses generics that other people wrote, such as Array, Map and Promise, and writes few of its own.

Remember: a blank should link two things. If it links nothing, take it out.

Part 6 of 7 · Practise

Three couriers, typed

Last item, swapped pair, and a box that maps.

Each problem starts with a JavaScript function that works for anything. Make it generic, so the types follow the values.

Problem 1

The last item

Make last generic, so a list of numbers gives back a number (or undefined), and a list of text gives back text (or undefined).

Start here

JavaScriptOpen in TypeScript Playground (opens in a new tab)
function last(items) {  return items[items.length - 1];}console.log(last([1, 2, 3])); // 3console.log(last(["a", "b"])); // "b"console.log(last([])); // undefined

Stuck?

Show a solution
TypeScriptOpen in TypeScript Playground (opens in a new tab)
function last<T>(items: T[]): T | undefined {  return items[items.length - 1];}last([1, 2, 3]); // 3last(["a", "b"]); // "b"last([]); // undefined

T[] links the list to its items, so the result is the same kind of thing the list holds. T | undefined is honest about the empty list, which JavaScript answered with undefined and no warning. A list of numbers gives number or undefined, and a list of text gives string or undefined.

Problem 2

Swap a pair

Make swap generic with two blanks, so swapping ["Asha", 21] gives the types [number, string].

Start here

JavaScriptOpen in TypeScript Playground (opens in a new tab)
function swap(entry) {  return [entry[1], entry[0]];}console.log(swap(["Asha", 21])); // [ 21, 'Asha' ]

Stuck?

Show a solution
TypeScriptOpen in TypeScript Playground (opens in a new tab)
function swap<A, B>(entry: [A, B]): [B, A] {  return [entry[1], entry[0]];}swap(["Asha", 21]); // [21, "Asha"]

Two blanks, A and B, keep the two slots apart. The input is [A, B] and the output is [B, A], so the types swap along with the values. With a single T, both slots would have to be the same kind.

Problem 3

Map over a box

Write the Box<T> type and make mapBox generic, so a box of text mapped with length becomes a box of numbers.

Start here

JavaScriptOpen in TypeScript Playground (opens in a new tab)
function mapBox(box, change) {  return { value: change(box.value) };}console.log(mapBox({ value: 21 }, (n) => n + 1)); // { value: 22 }console.log(mapBox({ value: "chai" }, (s) => s.length)); // { value: 4 }

Stuck?

Show a solution
TypeScriptOpen in TypeScript Playground (opens in a new tab)
type Box<T> = { value: T };function mapBox<T, U>(box: Box<T>, change: (value: T) => U): Box<U> {  return { value: change(box.value) };}mapBox({ value: 21 }, (n) => n + 1); // { value: 22 }mapBox({ value: "chai" }, (s) => s.length); // { value: 4 }

Box<T> is the parcel. The change function takes the old kind T and gives a new kind U, and the result is a Box<U>. So a box of text mapped with length becomes a box of numbers, and the checker tracks it. Two blanks are needed because the input and output kinds can differ.

Part 7 of 7 · Interview notes

Generics in the interview

Type parameters, constraints, and when to use them.

Generics questions are often the hardest in the round. These short answers cover most of them.

  • "What is a generic?" A function, type or class with a type parameter, a blank that is filled in when you use it. It lets one definition work for many types and still keep them straight.
  • "Why not just use any?" any turns the checker off, and the output type is lost. A generic keeps the link between input and output.
  • "What does T extends X mean?" A constraint. T can be any type, as long as it fits X. It lets you use X's properties inside the function.
  • "What is keyof?" An operator that gives the names of a type's properties as a union of exact words. K extends keyof T limits a key to real ones.
  • "Does TypeScript need <T> at the call?" Usually not. It infers T from the arguments. You write it yourself when nothing gives it enough to go on, like an empty list.
  • "What are examples of built-in generics?" Array<T>, Map<K, V>, Set<T> and Promise<T>.
  • "What is a default type parameter?" A fallback used when the blank is left empty, as in type Result<T = string>.
  • "When should you not use a generic?" When the blank links nothing, or when you only ever use one type.
  • Say it simply. "A generic is a function or type with a blank, so what goes in decides what comes out" is a good answer.

Updated 2026-10-17

Made by VeraStack Labs