We Use Mapped & Conditional Types in TypeScript (Real-World Examples)
When building real applications, we rarely deal with static types.
Data changes based on:
- API responses
- User roles
- Form states
- Backend validations
Mapped Types and Conditional Types help us reuse and transform existing types instead of rewriting them again and again.
Let’s understand why they exist, using simple real-world examples.
1. Why Mapped Types?
Problem (Real Life)
You have a User model coming from the backend:
type User = {
id: number
name: string
email: string
isActive: boolean
}
Now in real projects, you often need:
- A form version (all fields optional)
- A readonly version (API response)
- A preview version (only some fields)
Without mapped types ❌ you’d rewrite types manually — which is error-prone.
Solution: Mapped Types ✅
Example 1: Form State (All Optional)
type UserForm = {
[K in keyof User]?: User[K]
}
Now:
- Adding/removing fields in
Userautomatically updatesUserForm - No duplicate type maintenance
This is exactly what Partial<User> does internally.
Example 2: Read-Only API Response
type ReadonlyUser = {
readonly [K in keyof User]: User[K]
}
Why?
- Prevents accidental mutation of API data
- Very common in frontend apps
Example 3: Editable Fields Only
type EditableUser = {
[K in "name" | "email"]: User[K]
}
Used when:
- Only some fields are editable
- Others (like
id) should not be touched
2. Why Conditional Types?
Problem (Real Life)
Different APIs or functions return different data types based on input.
Example:
- If user is
"admin"→ return full access - If user is
"guest"→ limited access
Solution: Conditional Types ✅
Example 1: Role-Based Data
type Role = "admin" | "user"
type Permissions<T> = T extends "admin"
? { canDelete: true; canEdit: true }
: { canDelete: false; canEdit: true }
Usage:
type AdminPermissions = Permissions<"admin">
type UserPermissions = Permissions<"user">
Why this matters:
- Type safety at compile time
- No runtime role mistakes
Example 2: API Response Type
type ApiResponse<T> =
T extends "success"
? { data: string }
: { error: string }
type Success = ApiResponse<"success">
type Failure = ApiResponse<"error">
This matches how real APIs behave.
3. Why infer? (Very Practical Example)
Problem
You want to extract the data type from an API function automatically.
function fetchUser() {
return { id: 1, name: "Srashti" }
}
Manually writing types ❌ is repetitive.
Solution: infer ✅
type ExtractReturn<T> =
T extends (...args: any[]) => infer R ? R : never
type UserData = ExtractReturn<typeof fetchUser>
Why this is powerful:
- Auto-syncs with function changes
- No manual updates
This is how ReturnType<T> works internally.
4. Why Distributive Conditional Types?
Problem
You have a union type and want logic applied to each member.
type Input = string | number
Solution
type ToArray<T> = T extends any ? T[] : never
type Result = ToArray<string | number>
// string[] | number[]
Why this is useful:
- Validating inputs
- Transforming API payloads
- Working with union types cleanly
5. Real Use of Utility Types (Behind the Scenes)
All of these are built using Mapped + Conditional Types:
Partial<T>
Pick<T, K>
Omit<T, K>
Exclude<T, U>
Extract<T, U>
ReturnType<T>
Example from real apps:
type UpdateUserPayload = Omit<User, "id">
Used when:
- Backend auto-generates
id - Frontend shouldn’t send it
6. Real-World Summary (Why We Actually Use Them)
We use Mapped Types when:
- One base model has multiple variations
- Forms, DTOs, API models differ slightly
- We want DRY (Don’t Repeat Yourself) types
We use Conditional Types when:
- Output type depends on input
- Role-based or state-based logic
- API responses vary
Final Thought
Mapped Types and Conditional Types are not “advanced for no reason”.
They exist because real applications are dynamic, and TypeScript helps us model that safely and cleanly.
Once you start using them, you’ll:
- Write fewer bugs
- Remove duplicate types
Top comments (0)