TypeScript Generics: The Magic Box That Fits Everything! 🎁
The Story of the Magic Container
Imagine you have a magic box. This box is special because it can hold anything you put inside it—toys, cookies, books, or even your pet goldfish! But here’s the cool part: once you tell the box what it’s holding, it remembers and only gives you back that exact type of thing.
That’s what Generics are in TypeScript. They’re like magic containers that work with any type you choose, while keeping everything safe and organized.
🎯 Generic Functions
The Universal Helper
A generic function is like a helpful friend who can do the same task for any type of thing you give them.
Simple Example:
function getFirst<T>(items: T[]): T {
return items[0];
}
// Works with numbers!
const firstNum = getFirst([1, 2, 3]);
// Result: 1
// Works with strings too!
const firstWord = getFirst(["apple", "banana"]);
// Result: "apple"
What’s happening?
- The
<T>is like a placeholder label - When you call the function, TypeScript fills in
Twith the actual type - The function works the same way, no matter what type you use!
graph TD A["getFirst<T>"] --> B["T = number"] A --> C["T = string"] A --> D["T = any type!"] B --> E["Returns: number"] C --> F["Returns: string"] D --> G["Returns: that type"]
🏗️ Generic Interfaces
The Blueprint That Adapts
An interface is like a blueprint for building things. A generic interface is a blueprint that works for different materials.
Simple Example:
interface Box<T> {
content: T;
label: string;
}
// A box for toys
const toyBox: Box<string> = {
content: "Teddy Bear",
label: "My Toys"
};
// A box for numbers
const scoreBox: Box<number> = {
content: 100,
label: "High Score"
};
Real Life:
- Think of a shipping box label
- The label format stays the same
- But the contents can be anything!
🏛️ Generic Classes
The Factory That Makes Anything
A generic class is like a factory machine that can produce containers for any product.
Simple Example:
class Storage<T> {
private items: T[] = [];
add(item: T): void {
this.items.push(item);
}
getAll(): T[] {
return this.items;
}
}
// Storage for fruits
const fruitStorage = new Storage<string>();
fruitStorage.add("Apple");
fruitStorage.add("Banana");
// Storage for numbers
const numStorage = new Storage<number>();
numStorage.add(42);
numStorage.add(100);
Why is this cool?
- One class definition
- Works for strings, numbers, objects—anything!
- TypeScript checks that you only add the right type
🔒 Generic Constraints
Setting Some Rules
Sometimes your magic box needs rules. You can’t put a whale in a small box, right?
Constraints let you say: “This generic only works with types that have certain features.”
Simple Example:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
// Works! Strings have length
logLength("Hello"); // Output: 5
// Works! Arrays have length
logLength([1, 2, 3]); // Output: 3
// Error! Numbers don't have length
// logLength(42); ❌
The extends keyword says:
“T must have a length property!”
graph TD A["T extends HasLength"] --> B{"Does T have length?"} B -->|Yes| C["✅ Allowed"] B -->|No| D["❌ Error"]
🎭 Multiple Type Parameters
More Than One Placeholder
What if your magic box needs to hold two different things? Use multiple type parameters!
Simple Example:
function makePair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
// String and number pair
const pair1 = makePair("age", 25);
// Result: ["age", 25]
// Boolean and string pair
const pair2 = makePair(true, "yes");
// Result: [true, "yes"]
Naming Convention:
T= Type (most common)K= KeyV= ValueA, B, C= when you need multiple
🎁 Generic Defaults
The Backup Plan
Sometimes you want a default type if no one specifies one. Like a vending machine that gives you cola if you don’t choose.
Simple Example:
interface Response<T = string> {
data: T;
status: number;
}
// Uses default (string)
const res1: Response = {
data: "Hello!",
status: 200
};
// Override with number
const res2: Response<number> = {
data: 42,
status: 200
};
The = string part means:
“If you don’t tell me the type, I’ll use string!”
🔮 Generic Inference
TypeScript’s Smart Guessing
Here’s the magic part: TypeScript is really smart. It can figure out the type without you telling it!
Simple Example:
function wrap<T>(value: T): { wrapped: T } {
return { wrapped: value };
}
// No need to write wrap<string>(...)
const result = wrap("hello");
// TypeScript knows: result is { wrapped: string }
const numResult = wrap(123);
// TypeScript knows: numResult is { wrapped: number }
How does it work?
- You call
wrap("hello") - TypeScript sees
"hello"is a string - It automatically sets
T = string - No extra typing needed!
graph TD A["wrap#40;42#41;"] --> B["TypeScript sees: 42"] B --> C["Infers: T = number"] C --> D["Returns: { wrapped: number }"]
🔐 Const Type Parameters
Locking In the Exact Value
With const type parameters, TypeScript remembers the exact value, not just the general type.
Simple Example:
// Without const - generic type
function getItems<T extends readonly string[]>(
items: T
) {
return items;
}
const fruits = getItems(["apple", "banana"]);
// Type: string[] (general)
// With const - exact values locked!
function getExactItems<const T extends readonly string[]>(
items: T
) {
return items;
}
const exactFruits = getExactItems(["apple", "banana"]);
// Type: readonly ["apple", "banana"] (exact!)
Why use const?
- Normal: TypeScript says “it’s an array of strings”
- With const: TypeScript says “it’s EXACTLY these values in this order!”
Real Use Case:
function createMenu<const T extends readonly string[]>(
options: T
): T[number] {
return options[0];
}
const menu = createMenu(["Home", "About", "Contact"]);
// menu can ONLY be "Home" | "About" | "Contact"
// Not just any string!
🎯 Putting It All Together
Here’s a complete example using everything we learned:
// Generic interface with constraint
interface Identifiable {
id: number;
}
// Generic class with constraint + default
class DataStore<T extends Identifiable = { id: number }> {
private items: T[] = [];
// Generic method with inference
add(item: T): T {
this.items.push(item);
return item;
}
// Multiple type parameters
findAndTransform<R>(
id: number,
transform: (item: T) => R
): R | undefined {
const item = this.items.find(i => i.id === id);
return item ? transform(item) : undefined;
}
}
// Usage
interface User extends Identifiable {
name: string;
}
const userStore = new DataStore<User>();
userStore.add({ id: 1, name: "Alice" });
// TypeScript infers everything!
const greeting = userStore.findAndTransform(
1,
user => `Hello, ${user.name}!`
);
// greeting is: string | undefined
🌟 Quick Summary
| Concept | What It Does | Example |
|---|---|---|
| Generic Functions | Work with any type | function get<T>(x: T): T |
| Generic Interfaces | Flexible blueprints | interface Box<T> |
| Generic Classes | Reusable containers | class Store<T> |
| Constraints | Set type requirements | T extends SomeType |
| Multiple Params | Handle multiple types | <A, B, C> |
| Defaults | Fallback type | <T = string> |
| Inference | Smart type guessing | Automatic! |
| Const Params | Lock exact values | <const T> |
🎉 You Did It!
Generics might seem tricky at first, but remember:
Generics are just placeholders that TypeScript fills in for you.
Like a fill-in-the-blank game where TypeScript is really good at guessing the right answer!
Now go build something amazing with your new superpower! 🚀