
Advanced TypeScript Patterns for React Applications
TypeScript has revolutionized React development by providing static type checking and enhanced developer experience. In this comprehensive guide, we'll explore advanced TypeScript patterns that will elevate your React applications to new levels of type safety and maintainability.
Generic Components
Generic components are one of the most powerful patterns in TypeScript React development. They allow you to create reusable components that work with different data types while maintaining type safety.
Basic Generic Component Pattern
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
keyExtractor?: (item: T, index: number) => string | number;
}
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
return (
<ul>
{items.map((item, index) => (
<li key={keyExtractor ? keyExtractor(item, index) : index}>
{renderItem(item)}
</li>
))}
</ul>
);
}
// Usage example
const users = [
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' }
];
<List
items={users}
keyExtractor={(user) => user.id}
renderItem={(user) => <span>{user.name}</span>}
/>
Conditional Types in Components
Conditional types allow you to create components that adapt their behavior based on their props, enabling powerful type-driven patterns.
Advanced Conditional Type Example
type ButtonVariant = 'button' | 'link' | 'submit';
type ButtonProps<T extends ButtonVariant> = {
children: React.ReactNode;
disabled?: boolean;
className?: string;
} & (T extends 'link'
? {
variant: 'link';
href: string;
onClick?: never;
type?: never;
}
: T extends 'submit'
? {
variant: 'submit';
type: 'submit';
onClick?: () => void;
href?: never;
}
: {
variant: 'button';
onClick: () => void;
href?: never;
type?: never;
}
);
function Button<T extends ButtonVariant>(props: ButtonProps<T>) {
const baseClasses = `btn ${props.className || ''}`;
if (props.variant === 'link') {
return (
<a
href={props.href}
className={baseClasses}
aria-disabled={props.disabled}
>
{props.children}
</a>
);
}
return (
<button
type={props.variant === 'submit' ? 'submit' : 'button'}
onClick={props.onClick}
disabled={props.disabled}
className={baseClasses}
>
{props.children}
</button>
);
}
// Usage examples - all type-safe!
<Button variant="button" onClick={() => alert('clicked')}>
Click me
</Button>
<Button variant="link" href="/about">
Go to About
</Button>
<Button variant="submit">
Submit Form
</Button>
Type-Safe APIs
Creating type-safe APIs involves using TypeScript's type system to ensure that your API calls are correct at compile time.
API Client Pattern
// Define your API structure
interface User {
id: number;
name: string;
email: string;
}
interface CreateUserRequest {
name: string;
email: string;
}
interface UpdateUserRequest {
name?: string;
email?: string;
}
// Define endpoints and their methods
interface ApiEndpoints {
'/users': {
GET: User[];
POST: CreateUserRequest;
};
'/users/:id': {
GET: User;
PUT: UpdateUserRequest;
DELETE: void;
};
'/users/:id/avatar': {
POST: FormData;
GET: { url: string };
};
}
// Extract path parameters from endpoint strings
type ExtractParams<T extends string> =
T extends `${infer Start}:${infer Param}/${infer Rest}`
? { [K in Param]: string } & ExtractParams<`${Start}${Rest}`>
: T extends `${infer Start}:${infer Param}`
? { [K in Param]: string }
: {};
// Create the type-safe API client type
type ApiClient = {
[K in keyof ApiEndpoints]: {
[M in keyof ApiEndpoints[K]]: ExtractParams<K> extends Record<never, never>
? M extends 'GET' | 'DELETE'
? () => Promise<ApiEndpoints[K][M]>
: (body: ApiEndpoints[K][M]) => Promise<void>
: M extends 'GET' | 'DELETE'
? (params: ExtractParams<K>) => Promise<ApiEndpoints[K][M]>
: (params: ExtractParams<K>, body: ApiEndpoints[K][M]) => Promise<void>
}
};
// Implementation
class TypeSafeApiClient implements ApiClient {
constructor(private baseUrl: string) {}
'/users' = {
GET: async (): Promise<User[]> => {
const response = await fetch(`${this.baseUrl}/users`);
return response.json();
},
POST: async (body: CreateUserRequest): Promise<void> => {
await fetch(`${this.baseUrl}/users`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
}
};
'/users/:id' = {
GET: async (params: { id: string }): Promise<User> => {
const response = await fetch(`${this.baseUrl}/users/${params.id}`);
return response.json();
},
PUT: async (params: { id: string }, body: UpdateUserRequest): Promise<void> => {
await fetch(`${this.baseUrl}/users/${params.id}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
});
},
DELETE: async (params: { id: string }): Promise<void> => {
await fetch(`${this.baseUrl}/users/${params.id}`, {
method: 'DELETE'
});
}
};
'/users/:id/avatar' = {
GET: async (params: { id: string }): Promise<{ url: string }> => {
const response = await fetch(`${this.baseUrl}/users/${params.id}/avatar`);
return response.json();
},
POST: async (params: { id: string }, body: FormData): Promise<void> => {
await fetch(`${this.baseUrl}/users/${params.id}/avatar`, {
method: 'POST',
body
});
}
};
}
// Usage - all type-safe!
const api = new TypeSafeApiClient('https://api.example.com');
// ✅ Type-safe calls
const users = await api['/users'].GET();
await api['/users'].POST({ name: 'Alice', email: 'alice@example.com' });
const user = await api['/users/:id'].GET({ id: '123' });
await api['/users/:id'].PUT({ id: '123' }, { name: 'Alice Smith' });
// ❌ TypeScript will catch these errors:
// await api['/users'].GET({ id: '123' }); // Error: GET doesn't take params
// await api['/users/:id'].GET(); // Error: missing required params
// await api['/users'].POST({ invalid: 'field' }); // Error: wrong body type
Best Practices
Use strict TypeScript configuration
- Enable all strict type checking options- Leverage type inference - Let TypeScript infer types when possible
- Create reusable type utilities - Build a library of common type patterns
- Use branded types - Create distinct types for similar data structures
- Implement proper error boundaries - Handle errors at the type level
Conclusion
Advanced TypeScript patterns provide the foundation for building robust, maintainable React applications. By mastering these patterns, you'll create code that is not only type-safe but also self-documenting and easier to refactor.