Generics
Reviewed & published by Brayan K
A generic is a type variable — a placeholder like <T> that the caller fills in — letting you write one reusable function, interface, or class that stays fully type-safe for every type it's used with.
Part of the free TypeScript course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.
What You'll Learn
- Write generic functions with a <T> type parameter
- Let TypeScript infer the type parameter from your arguments
- Use multiple type parameters like <A, B> and <T, U>
- Build generic interfaces and classes (Box<T>, Stack<T>)
- Constrain a type with T extends ... to access its members
- See exactly why generics beat any for reusable code
💡 Real-World Analogy
A generic is a shipping box that adapts to its contents. The box itself doesn't care whether it holds books or mugs, but once you put books in, the label says "books" and you'll get books back out — not a vague "stuff". Writing Box<T> is choosing that label at packing time, so everything stays correctly typed end to end instead of being dumped into an untyped any pile.
1. Generic Functions
A generic function declares a type variable in angle brackets — function identity<T>(value: T): T — and uses it in the parameters and return type. The key benefit is the link it preserves: whatever type you pass in is the exact type you get back. Most of the time you won't even write the <T> at the call site, because TypeScript infers it from your argument.
// The type variable <T> is filled in PER CALL:
function identity<T>(value: T): T {
return value;
}
identity<number>(42); // T = number (usually inferred, so just identity(42))
identity("hello"); // T = string (inferred from the argument)
function first<T>(arr: T[]): T | undefined {
return arr[0]; // returns the element type, not 'any'
}
const n = first([10, 20]); // n: number | undefined
function wrap<T>(value: T): T[] {
return [value];
}
const w = wrap("x"); // w: string[]// A GENERIC is a TYPE VARIABLE — a placeholder for a type the CALLER chooses.
// In TypeScript: function identity<T>(value: T): T { return value; }
// The <T> means "whatever type you pass in, you get the SAME type back".
// At runtime the type vanishes; the function just returns its argument.
function identity(value) {
return value; // <T>(value: T): T — type preserved by the compiler
}
console.log(identity(42)); // 42
console.log(identity("hello")); // hello
console.log(identity(true)); // true
// 'first' returns the first element of an array, keeping its element type:
// function first<T>(arr: T[]): T | undefined { return arr[0]; }
function first(arr) {
return arr[0];
}
console.log(first([10, 20, 30])); // 10
console.log(first(["a", "b"])); // a
console.log(first([])); // undefined
// 'wrap' puts a value into a single-element array, keeping its type:
// function wrap<T>(value: T): T[] { return [value]; }
function wrap(value) {
return [value];
}
console.log(wrap(7)); // [ 7 ]
console.log(wrap("x")); // [ 'x' ]2. Multiple Type Parameters & Inference
A generic can take several independent type variables, written <A, B> or <T, U>. A function like pair<A, B>(a: A, b: B): [A, B] keeps both types distinct, and a transform like mapValues<T, U> lets the input and output element types differ. TypeScript infers all of them from the arguments you actually pass.
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("age", 30); // p: [string, number]
function mapValues<T, U>(arr: T[], fn: (x: T) => U): U[] {
return arr.map(fn);
}
const lengths = mapValues(["a", "bb"], s => s.length); // T=string, U=number -> number[]// Generics can take MULTIPLE type parameters.
// In TypeScript: function pair<A, B>(a: A, b: B): [A, B] { return [a, b]; }
// Each letter is its own independent type variable.
function pair(a, b) {
return [a, b]; // <A, B>(a: A, b: B): [A, B]
}
console.log(pair("age", 30)); // [ 'age', 30 ]
console.log(pair(true, [1, 2])); // [ true, [ 1, 2 ] ]
// 'mapValues' applies a function to each item — INFERRING both the input
// and output element types:
// function mapValues<T, U>(arr: T[], fn: (x: T) => U): U[] { ... }
function mapValues(arr, fn) {
const out = [];
for (const item of arr) out.push(fn(item));
return out;
}
console.log(mapValues([1, 2, 3], (n) => n * 10)); // [ 10, 20, 30 ]
console.log(mapValues(["a", "bb"], (s) => s.length)); // [ 1, 2 ]
// In a .ts file the compiler infers T and U from the arguments:
// mapValues([1,2,3], n => n * 10) // T = number, U = number3. Generic Interfaces, Classes & Constraints
Types themselves can be generic. interface Box<T> { value: T } and class Stack<T> are parameterised by a type you choose when you use them, so a Stack<number> only accepts numbers. A constraint — <T extends { length: number }> — restricts what T may be so you can safely use specific members of it inside the function.
interface Box<T> { value: T }
const b: Box<string> = { value: "hi" };
class Stack<T> {
private items: T[] = [];
push(x: T): this { this.items.push(x); return this; }
pop(): T | undefined { return this.items.pop(); }
}
const s = new Stack<number>();
s.push(1).push(2);
// Constraint: T must have a .length, so reading .length is allowed:
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}// A GENERIC INTERFACE / CLASS is parameterised by a type too.
// In TypeScript:
// interface Box<T> { value: T; }
// class Stack<T> { private items: T[] = []; push(x: T) {...} pop(): T | undefined {...} }
// A Stack that holds one chosen type, demonstrated at runtime:
class Stack {
constructor() { this.items = []; }
push(x) { this.items.push(x); return this; }
pop() { return this.items.pop(); }
size() { return this.items.length; }
}
const numbers = new Stack(); // in TS: new Stack<number>()
numbers.push(1).push(2).push(3);
console.log("size:", numbers.size()); // size: 3
console.log("pop:", numbers.pop()); // pop: 3
console.log("pop:", numbers.pop()); // pop: 2
// CONSTRAINTS limit what T can be: <T extends { length: number }>
// means "T must have a .length". Then .length is safe to read:
// function longest<T extends { length: number }>(a: T, b: T): T { ... }
function longest(a, b) {
return a.length >= b.length ? a : b;
}
console.log(longest("hello", "hi")); // hello
console.log(longest([1, 2], [1, 2, 3])); // [ 1, 2, 3 ]
// In a .ts file:
// interface Box<T> { value: T }
// class Stack<T> { ... }
// function longest<T extends { length: number }>(a: T, b: T): T { ... }🎯 Your Turn
Write a generic last that returns the final element of an array (or undefined for an empty one). Fill in the two blanks marked ___, then run it.
// 🎯 YOUR TURN — replace each ___ then press "Try it Yourself".
// 'last' is a generic that returns the LAST element of an array,
// preserving the element type: function last<T>(arr: T[]): T | undefined
function last(arr) {
return arr[arr.___ - 1]; // 👉 use .length to find the last index
}
console.log(last([10, 20, 30])); // 30
console.log(last(["x", "y"])); // y
console.log(last(___)); // 👉 pass [] to get undefined
// ✅ Expected output:
// 30
// y
// undefinedCommon Errors (and the fix)
- ❌ "Property 'length' does not exist on type 'T'" — you used a member without a constraint. ✅ Add <T extends { length: number }> so the compiler knows it exists.
- ❌ Reaching for any "to make it generic" — that throws away every type. ✅ Use a real type parameter <T>; you get reuse and safety.
- ❌ Over-annotating the call — writing identity<string>("x") everywhere is noisy. ✅ Let inference do it: just identity("x").
- ❌ "Type 'T' is not assignable to type 'U'" — you mixed up two distinct type variables. ✅ Remember T and U are independent; only equate them if they're truly the same type.
Pro Tips
- 💡 Name type params meaningfully when it aids clarity — <TKey, TValue> reads better than <K, V> in complex code.
- 💡 Constrain, don't widen — a precise extends bound documents intent and unlocks member access.
- 💡 Lean on inference — let the compiler fill in type arguments; only specify them when it genuinely can't.
- 💡 Built-in generics are everywhere — Array<T>, Promise<T>, Map<K, V>, Record<K, V> — recognising them makes library types far less scary.
Mini-Challenge: A Generic groupBy
No blanks this time — just a brief and a starting outline. Build the generic helper yourself, run it, and check your output against the example in the comments.
// 🎯 MINI-CHALLENGE: a generic 'groupBy' helper
// In TypeScript: function groupBy<T>(items: T[], keyOf: (x: T) => string): Record<string, T[]>
// It groups array items into buckets keyed by a function you supply.
//
// 1. Write groupBy(items, keyOf) that returns an object.
// 2. For each item, compute key = keyOf(item); push the item into result[key]
// (create an empty array for a key the first time you see it).
// 3. Group these words by their first letter and log the result:
// ["apple", "avocado", "banana", "cherry"]
//
// ✅ Expected output:
// { a: [ 'apple', 'avocado' ], b: [ 'banana' ], c: [ 'cherry' ] }
// your code here🎉 Lesson Complete
- ✅ A generic is a type variable the caller fills in, preserving type safety
- ✅ Generic functions like identity<T> keep the input type linked to the output
- ✅ Multiple params (<A, B>, <T, U>) and inference work together automatically
- ✅ Interfaces and classes can be generic: Box<T>, Stack<T>
- ✅ A constraint T extends ... lets you use specific members of T
Practice quiz
What is a generic in TypeScript?
- A function that returns any
- A way to disable type checking
- A type variable the caller fills in
- A runtime cast operator
Answer: A type variable the caller fills in. A generic like <T> is a type variable: a placeholder for a type the caller chooses.
What does function identity<T>(value: T): T guarantee?
- The return type matches the type passed in
- It always returns a string
- The argument is converted to any
- The function runs faster
Answer: The return type matches the type passed in. identity<T> links input to output, so whatever type you pass in is the exact type you get back.
Why are generics preferred over any?
- any is a syntax error
- any cannot be used in functions
- Generics run at runtime, any does not
- Generics preserve type safety while any throws it away
Answer: Generics preserve type safety while any throws it away. any switches off type checking; a generic keeps the input-output relationship type-checked.
What is the return type of first<T>(arr: T[]): T | undefined when called as first([10, 20])?
- any
- number | undefined
- number
- T
Answer: number | undefined. The generic remembers the element type, so the result is number | undefined, not any.
Do you usually have to write the type argument explicitly, as in identity<string>("hi")?
- No, TypeScript usually infers it from the argument
- Yes, always
- Only for numbers
- Only inside classes
Answer: No, TypeScript usually infers it from the argument. TypeScript infers the type parameter from your arguments, so identity("hi") infers T = string.
What does a constraint like <T extends { length: number }> do?
- Forces T to be a number
- Removes the length property
- Limits T to types that have a length property
- Makes T optional
Answer: Limits T to types that have a length property. The constraint requires T to have a .length, so reading a.length inside is safe.
How many independent type parameters can a generic declare?
- Exactly one
- Several, like <A, B> or <T, U>
- At most two
- Unlimited but they must be equal
Answer: Several, like <A, B> or <T, U>. A generic can take multiple independent type variables, each its own placeholder.
In mapValues<T, U>(arr: T[], fn: (x: T) => U): U[], what are T and U?
- The same type always
- Both must be strings
- Runtime values, not types
- Input element type and output element type
Answer: Input element type and output element type. T is the input element type and U the output element type; they are independent and inferred.
What is true of generic type parameters at runtime?
- They are enforced with runtime checks
- They are erased; generics are compile-time only
- They become hidden function arguments
- They slow the program down
Answer: They are erased; generics are compile-time only. Type parameters disappear at runtime; generics are a purely compile-time feature.
Which of these is a built-in generic type in TypeScript?
- console.log
- typeof
- Promise<T>
- switch
Answer: Promise<T>. Array<T>, Promise<T>, Map<K, V>, and Record<K, V> are all built-in generics.
Continue this course
- Previous: Type Narrowing & Type Guards
- Next: Object Types: Optional, Readonly & Index Signatures — Model flexible objects with ?, readonly, and index signatures
- Quick reference: TypeScript cheat sheet › Functions & Generics
Frequently asked questions
What problem do generics actually solve?
They let you write ONE function or type that works for many types WITHOUT losing type safety. A generic identity<T> returns exactly the type you pass in, so identity(42) is known to be a number. The alternative, using any, would compile but throw away all type information — defeating the point of TypeScript.
Why are generics better than 'any'?
any switches off type checking entirely: the result of an any function is unchecked everywhere it flows. A generic preserves the relationship between inputs and outputs, so the compiler still catches misuse downstream. Generics give you reuse AND safety; any gives you reuse by removing safety.
Do I have to write the type in angle brackets every time?
Usually not. TypeScript infers the type parameter from your arguments — identity("hi") infers T = string automatically. You only write it explicitly (identity<string>(...)) when inference can't determine it or when you want to force a particular type.
What is a generic constraint (T extends ...)?
A constraint limits which types T may be. Writing <T extends { length: number }> means 'T must have a length property', which lets you safely read .length inside the function. Without the constraint, TypeScript only knows T could be anything, so it blocks property access.