When you build real-world apps, flexibility matters. You don’t want to rewrite the same logic for different types. You want to write once, use everywhere — safely.
That’s what generics in TypeScript are for.
They let you build type-safe, reusable, and intelligent functions, components, and data structures — without giving up precision.
Let’s break them down with real-world patterns you’ll actually use.
🔄 What are generics?
Generics are type placeholders — they let you write functions or components where the type is decided later.
Think of it as saying:
> “I don’t know the exact type yet, but whatever it is, I’ll work with it safely.”
Basic example:
function identity<T>(value: T): T {
return value
}
identity(42) // type: number
identity('hello') // type: string
Here, T
is a generic type variable. When you call the function, TypeScript infers the type for you.
💡 Why not just use any
?
Because any
removes all type safety.
function identity(value: any): any {
return value.toUpperCase() // no error — even if it's a number
}
Generics preserve type information, so you get:
- Autocomplete
- Static checks
- Safer refactoring
🧰 Real-world use cases
1. Reusable utility functions
function getFirst<T>(list: T[]): T | undefined {
return list[0]
}
getFirst([1, 2, 3]) // type: number
getFirst(['a', 'b']) // type: string
Works with any type — without losing context.
2. Generic props in react
type DropdownProps<T> = {
options: T[]
onSelect: (value: T) => void
}
function Dropdown<T>({ options, onSelect }: DropdownProps<T>) {
return (
<select onChange={e => onSelect(options[e.target.selectedIndex])}>
{options.map((opt, i) => (
<option key={i}>{String(opt)}</option>
))}
</select>
)
}
🟢 Now you can reuse this component for string[]
, number[]
, or even User[]
— and still get full type safety.
3. API Response wrappers
type ApiResponse<T> = {
data: T
success: boolean
error?: string
}
// Usage:
const response: ApiResponse<User[]> = {
data: [{ id: 1, name: 'Jane' }],
success: true,
}
🟢 Works for any backend shape, while maintaining strict types.
⛓️ Constraints (extends
)
You can limit what a generic type can be:
function getLength<T extends { length: number }>(item: T) {
return item.length
}
getLength('hello') // ✅
getLength([1, 2, 3]) // ✅
getLength(123) // ❌ number doesn't have .length
This helps you express:
> “I'll accept any type, as long as it has this shape.”
🧠 Tips for clean generic code
- Use short names like
T
,U
,K
,V
when the context is obvious - Be explicit when inference fails:
identity<string>('hello')
- Avoid overcomplicating — don’t add generics if a union or overload works better
- Combine with utility types like
Partial<T>
,Pick<T>
,Record<K, T>
❓ When not to use generics
Generics add complexity. You don’t need them when:
- The function/component is truly type-specific
- A union type would work better (
string | number
) - You’re not returning or using the input type meaningfully
🧩 The bottom line
Generics let your code adapt without becoming loose or fragile. They’re TypeScript’s answer to reusability with guardrails.
Use them for:
- Building reusable logic
- Wrapping backend responses
- Designing flexible components
- Working with collections
And always remember: you’re not just writing code — you’re shaping the developer experience and protecting the user experience. Generics help you do both, at scale.