Made by VeraStack Labs
Contents
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.
Anything in, who knows what out
JavaScript carries the parcel, and forgets the label.
A JavaScript courier is one line.
function deliver(parcel) { return parcel;}const sweets = deliver("laddoo");const count = deliver(42);console.log(sweets.toUpperCase());console.log(count + 1);LADDOO
43It 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.
function first(items) { return items[0];}const firstScore = first([90, 85, 72]);const firstName = first(["Asha", "Ravi"]);console.log(firstScore);console.log(firstScore.toUpperCase());90
TypeError: firstScore.toUpperCase is not a functionThe 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.
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.
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.
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.
function pair<A, B>(left: A, right: B): [A, B] { return [left, right];}const entry = pair("Asha", 21);console.log(entry);[ '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".
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);undefined2. 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 20Tinted 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.
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]));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.
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"));Asha
21getField(user, "name") is a string and getField(user, "age") is a number. A misspelled key is caught.
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.
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());8
CALL AMMAYou 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.
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.
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>.
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.
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.
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
function last(items) { return items[items.length - 1];}console.log(last([1, 2, 3])); // 3console.log(last(["a", "b"])); // "b"console.log(last([])); // undefinedStuck?
Show a solution
function last<T>(items: T[]): T | undefined { return items[items.length - 1];}last([1, 2, 3]); // 3last(["a", "b"]); // "b"last([]); // undefinedT[] 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
function swap(entry) { return [entry[1], entry[0]];}console.log(swap(["Asha", 21])); // [ 21, 'Asha' ]Stuck?
Show a solution
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
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
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.
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?"anyturns the checker off, and the output type is lost. A generic keeps the link between input and output. - "What does
T extends Xmean?" A constraint.Tcan be any type, as long as it fitsX. It lets you useX'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 Tlimits a key to real ones. - "Does TypeScript need
<T>at the call?" Usually not. It infersTfrom 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>andPromise<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