tf
typefirst
  • Apps
  • Blog
  • Docs
  • Contributors
  • Community
tf
typefirst
  1. Home
  2. /Article
  3. /Advanced Typescript Patterns React

Our Blog

Modern web development insights and tutorials built with Next.js, React, and TypeScript.

Navigation

  • Home
  • All Articles
  • Sitemap

Technology

Next.js 15React 19TypeScriptTailwind CSSVercel

© 2024 Our Blog. Built with Next.js and Islands Architecture.

HomeArticlesCommunity
Advanced TypeScript Patterns for React Applications

Advanced TypeScript Patterns for React Applications

Published December 15, 2024

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.

Try the Multi-file Starter in Type Explorer

/labs/type-explorer/starter

A minimal set of TypeScript files demonstrating basic functionality to start exploring with our editor.

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

components/GenericList.tsx
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

components/ConditionalButton.tsx
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

lib/ApiClient.ts
// 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.

Tags patterns, type-safety, advanced