TypeScript Advanced Types: Generics, Discriminated Unions & Mapped Types
The Problem You're Solving
Your function accepts any value but doesn't know what type it is:
// ā Bad: No type safety
function processData(data: any) {
return data.name; // What if data is a number?
}
// ā
Good: Type-safe and reusable
function processData<T extends { name: string }>(data: T): string {
return data.name; // TypeScript knows data has 'name'
}
That difference = runtime errors vs compile-time type checking.
Without advanced types, you lose TypeScript's power. With them, you write code that's safe, reusable, and self-documenting.
TypeScript mastery appears in 28% of full-stack interviews and prevents 60% of production bugs.
Generics: Reusable Type-Safe Code
Basic Generic Function
// ā Limited - Works with any type but type-unsafe
function getFirstElement(arr: any[]): any {
return arr[0];
}
// ā
Generic - Type-safe and reusable
function getFirstElement<T>(arr: T[]): T {
return arr[0];
}
// Usage
const num = getFirstElement([1, 2, 3]); // num is number
const str = getFirstElement(['a', 'b']); // str is string
Generic Constraints
// ā Problem: Can't call .length on unknown type T
function getLength<T>(arr: T[]): number {
return arr.length; // OK for arrays
}
getLength('hello'); // Error! string is not T[]
// ā
Solution 1: Constrain to array
function getLength<T extends any[]>(arr: T): number {
return arr.length;
}
// ā
Solution 2: Constrain to has-length property
function getLength<T extends { length: number }>(arr: T): number {
return arr.length;
}
getLength('hello'); // ā
OK - string has length
getLength([1, 2, 3]); // ā
OK - array has length
Multiple Generics
// Store key-value pairs with type safety
function createEntry<K, V>(key: K, value: V): [K, V] {
return [key, value];
}
const entry = createEntry('name', 'Alice');
// entry is [string, string]
const mixed = createEntry(1, true);
// mixed is [number, boolean]
Conditional Types: Logic in Types
// Basic conditional: Type ? TrueType : FalseType
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<42>; // false
Real Example: Extracting Return Types
// Extract the return type of any function
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
function greet(name: string): string {
return `Hello, ${name}`;
}
type GreetReturn = ReturnType<typeof greet>; // string
Extracting Function Arguments
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
type MyFunctionParams = Parameters<typeof greet>; // [name: string]
Mapped Types: Transform Types
Basic Mapped Type
// Create a readonly version of any type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
interface User {
name: string;
age: number;
}
type ReadonlyUser = Readonly<User>;
// Result:
// {
// readonly name: string;
// readonly age: number;
// }
Getters and Setters
// Create getters for all properties
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User {
name: string;
age: number;
}
type UserGetters = Getters<User>;
// Result:
// {
// getName: () => string;
// getAge: () => number;
// }
Discriminated Unions: Type-Safe Pattern Matching
Problem: Ambiguous Union Types
// ā Problem: Can't know which shape
type Response = { status: 'success'; data: string } | { status: 'error'; error: string };
function handleResponse(res: Response) {
console.log(res.data); // Error! data not on error variant
}
Solution: Discriminated Union
// ā
Use a discriminator property
type Response =
| { status: 'success'; data: string }
| { status: 'error'; error: string };
function handleResponse(res: Response) {
if (res.status === 'success') {
console.log(res.data); // ā
Now valid!
} else {
console.log(res.error); // ā
Valid for error type
}
}
Real Example: API Responses
type ApiResponse<T> =
| { status: 200; data: T }
| { status: 400; error: string; details: Record<string, string> }
| { status: 500; error: string };
async function fetchUser(id: number) {
const res: ApiResponse<{ name: string }> = await fetch(`/api/users/${id}`).then(r => r.json());
switch (res.status) {
case 200:
console.log('User:', res.data.name); // ā
Has data
break;
case 400:
console.log('Validation error:', res.details); // ā
Has details
break;
case 500:
console.log('Server error:', res.error); // ā
Has error
break;
}
}
Template Literal Types
// Extract method from string
type ParseRoute<T extends string> = T extends `/${infer R}` ? R : T;
type UserRoute = ParseRoute<'/users'>; // 'users'
// Create variants
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiRoute = `${HTTPMethod} /api/${'users' | 'posts' | 'comments'}`;
// Expands to:
// 'GET /api/users' | 'GET /api/posts' | ...
Utility Types: Built-in Helpers
Partial - Make all properties optional
interface User {
name: string;
email: string;
age: number;
}
type PartialUser = Partial<User>;
// {
// name?: string;
// email?: string;
// age?: number;
// }
const update: PartialUser = { name: 'Alice' }; // ā
Valid
Pick<T, K> - Select specific properties
type UserPreview = Pick<User, 'name' | 'email'>;
// {
// name: string;
// email: string;
// }
Omit<T, K> - Exclude specific properties
type UserWithoutAge = Omit<User, 'age'>;
// {
// name: string;
// email: string;
// }
Record<K, T> - Object with specific keys
type PageRoles = Record<'admin' | 'user' | 'guest', boolean>;
// {
// admin: boolean;
// user: boolean;
// guest: boolean;
// }
const roles: PageRoles = {
admin: true,
user: false,
guest: false
};
Exclude<T, U> & Extract<T, U> - Union manipulation
type Status = 'success' | 'error' | 'pending';
type ErrorStatus = Extract<Status, 'error'>; // 'error'
type NonErrorStatus = Exclude<Status, 'error'>; // 'success' | 'pending'
Common Mistakes
ā Mistake 1: Over-constraining Generics
// WRONG - Too restrictive
function processUser<T extends User>(user: T): string {
return user.name; // Why use generic if you need User?
}
// CORRECT - Generic only when needed
function processUser<T extends { name: string }>(obj: T): string {
return obj.name; // Any object with name works
}
processUser({ name: 'Alice', role: 'admin' }); // ā
ā Mistake 2: Forgetting as const in Discriminators
// WRONG - status is string, not literal
function createResponse(success: boolean) {
return {
status: success ? 'success' : 'error', // string, not 'success' | 'error'
data: {}
};
}
// CORRECT - Use `as const` for literals
function createResponse(success: boolean) {
return {
status: success ? 'success' as const : 'error' as const,
data: {}
};
}
ā Mistake 3: Circular Type References
// WRONG - Infinite recursion
type Node = {
value: string;
children: Node[]; // References itself infinitely
};
// CORRECT - Still works! TypeScript handles this
type Node = {
value: string;
children: Node[]; // ā
Valid
};
FAQ: Advanced Types Mastery
Q1: When should I use generics?
A: When you want reusable code that works with multiple types.
// ā
Good use: Generic wrapper
type ApiResponse<T> = {
status: number;
data: T;
};
const userResponse: ApiResponse<User> = { status: 200, data: { name: 'Alice' } };
const postResponse: ApiResponse<Post> = { status: 200, data: { title: 'Hello' } };
// ā Bad use: Single type
type UserResponse<T> = { // Why generic if T is always User?
status: number;
data: User;
};
Q2: What's the difference between interface and type?
A: Mostly similar, but subtle differences.
// interface - Can merge, good for objects
interface User {
name: string;
}
interface User { // ā
Merges with above
age: number;
}
// User now has both name and age
// type - Can't merge, better for unions/tuples
type User = { name: string } | { id: number }; // Union type
type Coordinate = [number, number]; // Tuple
Use: interface for objects, type for unions/advanced types.
Q3: How do I create a type-safe event emitter?
A: Use generics and discriminated unions.
type EventMap = {
'user:login': { userId: number };
'user:logout': { timestamp: number };
'message:send': { text: string };
};
class EventEmitter {
private listeners: Map<string, Function[]> = new Map();
on<K extends keyof EventMap>(event: K, listener: (data: EventMap[K]) => void) {
if (!this.listeners.has(event)) {
this.listeners.set(event, []);
}
this.listeners.get(event)!.push(listener);
}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {
this.listeners.get(event)?.forEach(fn => fn(data));
}
}
const emitter = new EventEmitter();
// ā
Type-safe: data must match event
emitter.on('user:login', (data) => {
console.log(data.userId); // ā
Has userId
});
emitter.emit('user:login', { userId: 123 }); // ā
Valid
emitter.emit('user:login', { timestamp: 123 }); // ā Error! Wrong shape
Q4: Interview Question: Implement a type-safe form validation.
A: Here's production-ready approach:
type FieldValue = string | number | boolean;
type ValidationRule<T> = {
validate: (value: T) => boolean | Promise<boolean>;
message: string;
};
type FormSchema = {
[key: string]: FieldValue;
};
class FormValidator<T extends FormSchema> {
private rules: Map<keyof T, ValidationRule<any>[]> = new Map();
addRule<K extends keyof T>(field: K, rule: ValidationRule<T[K]>) {
if (!this.rules.has(field)) {
this.rules.set(field, []);
}
this.rules.get(field)!.push(rule);
}
async validate(data: T): Promise<Map<keyof T, string[]>> {
const errors: Map<keyof T, string[]> = new Map();
for (const [field, rules] of this.rules.entries()) {
const value = data[field];
const fieldErrors: string[] = [];
for (const rule of rules) {
const isValid = await rule.validate(value);
if (!isValid) {
fieldErrors.push(rule.message);
}
}
if (fieldErrors.length > 0) {
errors.set(field, fieldErrors);
}
}
return errors;
}
}
// Usage
interface LoginForm {
email: string;
password: string;
}
const validator = new FormValidator<LoginForm>();
validator.addRule('email', {
validate: (v) => /\S+@\S+\.\S+/.test(v),
message: 'Invalid email'
});
validator.addRule('password', {
validate: (v) => v.length >= 8,
message: 'Password must be 8+ characters'
});
Q5: Distributed Conditional Types - What are they?
A: Conditionals that distribute over union members.
// Without distributive property
type Flatten<T> = T extends any[] ? T[number] : T;
// With union - applies to each member
type Str = Flatten<string[]>; // string
type Num = Flatten<(number | boolean)[]>; // number | boolean
// Wrap in array to prevent distribution
type NonDistributive<T> = T extends any[] ? T[number] : T;
Conclusion
Advanced TypeScript types:
- Generics - Write reusable type-safe code
- Conditional Types - Add logic to types
- Mapped Types - Transform types systematically
- Discriminated Unions - Pattern match on types
- Template Literals - Type string patterns
Master these and you'll write code that's impossible to use incorrectly.