Advanced Patterns You'll Master
- • Generic components with type constraints
- • Conditional types for flexible APIs
- • Utility types for prop manipulation
- • Template literal types for type-safe strings
- • Advanced inference patterns
- • Real-world banking app examples
Why Advanced TypeScript Matters
After building React applications for banking systems that serve millions of users, I've learned that advanced TypeScript patterns aren't just academic exercises—they're essential for creating maintainable, bug-free applications at scale.
This guide covers the TypeScript patterns that have proven most valuable in production environments, with real examples from financial applications where type safety isn't optional.
Generic Components
Type-Safe Data Tables
Create reusable components that maintain type safety across different data shapes.
interface Column<T> {
key: keyof T;
title: string;
render?: (value: T[keyof T], record: T) => React.ReactNode;
sortable?: boolean;
width?: number;
}
interface DataTableProps<T> {
data: T[];
columns: Column<T>[];
onRowClick?: (record: T) => void;
loading?: boolean;
}
function DataTable<T extends Record<string, any>>({
data,
columns,
onRowClick,
loading
}: DataTableProps<T>) {
if (loading) {
return <TableSkeleton />;
}
return (
<table className="w-full border-collapse">
<thead>
<tr>
{columns.map(column => (
<th key={String(column.key)} style={{ width: column.width }}>
{column.title}
</th>
))}
</tr>
</thead>
<tbody>
{data.map((record, index) => (
<tr
key={index}
onClick={() => onRowClick?.(record)}
className="hover:bg-gray-50 cursor-pointer"
>
{columns.map(column => (
<td key={String(column.key)}>
{column.render
? column.render(record[column.key], record)
: String(record[column.key])
}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}
// Usage with full type safety
interface Transaction {
id: string;
amount: number;
currency: string;
date: Date;
status: 'pending' | 'completed' | 'failed';
}
const transactionColumns: Column<Transaction>[] = [
{ key: 'id', title: 'Transaction ID', width: 150 },
{
key: 'amount',
title: 'Amount',
render: (amount, record) => `${record.currency} ${amount.toFixed(2)}`
},
{
key: 'status',
title: 'Status',
render: (status) => (
<span className={`status-${status}`}>{status}</span>
)
}
];
function TransactionsPage() {
const [transactions] = useState<Transaction[]>([]);
return (
<DataTable
data={transactions}
columns={transactionColumns}
onRowClick={(transaction) => {
// transaction is fully typed as Transaction
console.log('Clicked transaction:', transaction.id);
}}
/>
);
}Generic Form Hooks
type ValidationRule<T> = {
required?: boolean;
validator?: (value: T) => string | null;
};
type FormConfig<T> = {
[K in keyof T]: ValidationRule<T[K]>;
};
type FormErrors<T> = {
[K in keyof T]?: string;
};
function useForm<T extends Record<string, any>>(
initialValues: T,
config: FormConfig<T>
) {
const [values, setValues] = useState<T>(initialValues);
const [errors, setErrors] = useState<FormErrors<T>>({});
const [touched, setTouched] = useState<Partial<Record<keyof T, boolean>>>({});
const setValue = <K extends keyof T>(key: K, value: T[K]) => {
setValues(prev => ({ ...prev, [key]: value }));
// Clear error when user starts typing
if (errors[key]) {
setErrors(prev => ({ ...prev, [key]: undefined }));
}
};
const validateField = <K extends keyof T>(key: K): string | null => {
const value = values[key];
const rules = config[key];
if (rules.required && (!value || value === '')) {
return `${String(key)} is required`;
}
if (rules.validator && value) {
return rules.validator(value);
}
return null;
};
const validateForm = (): boolean => {
const newErrors: FormErrors<T> = {};
let isValid = true;
Object.keys(config).forEach(key => {
const error = validateField(key as keyof T);
if (error) {
newErrors[key as keyof T] = error;
isValid = false;
}
});
setErrors(newErrors);
return isValid;
};
const handleSubmit = (onSubmit: (values: T) => void) => {
return (e: React.FormEvent) => {
e.preventDefault();
if (validateForm()) {
onSubmit(values);
}
};
};
return {
values,
errors,
touched,
setValue,
validateField,
validateForm,
handleSubmit,
setTouched
};
}
// Usage
interface LoginForm {
email: string;
password: string;
}
function LoginComponent() {
const form = useForm<LoginForm>(
{ email: '', password: '' },
{
email: {
required: true,
validator: (email) => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
return emailRegex.test(email) ? null : 'Invalid email format';
}
},
password: {
required: true,
validator: (password) => {
return password.length >= 8 ? null : 'Password must be at least 8 characters';
}
}
}
);
return (
<form onSubmit={form.handleSubmit((values) => console.log(values))}>
<input
type="email"
value={form.values.email}
onChange={(e) => form.setValue('email', e.target.value)}
/>
{form.errors.email && <span className="error">{form.errors.email}</span>}
<input
type="password"
value={form.values.password}
onChange={(e) => form.setValue('password', e.target.value)}
/>
{form.errors.password && <span className="error">{form.errors.password}</span>}
<button type="submit">Login</button>
</form>
);
}Conditional Types
API Response Types
Create flexible type definitions that adapt based on input parameters.
// Base API response structure
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}
// Different response types based on endpoint
type UserResponse = ApiResponse<{
id: string;
name: string;
email: string;
}>;
type TransactionsResponse = ApiResponse<{
transactions: Transaction[];
pagination: {
page: number;
total: number;
hasNext: boolean;
};
}>;
// Conditional type for API endpoints
type EndpointResponse<T> =
T extends 'user' ? UserResponse :
T extends 'transactions' ? TransactionsResponse :
T extends 'balance' ? ApiResponse<{ balance: number; currency: string }> :
never;
// Type-safe API client
class ApiClient {
async get<T extends 'user' | 'transactions' | 'balance'>(
endpoint: T
): Promise<EndpointResponse<T>> {
const response = await fetch(`/api/${endpoint}`);
return response.json();
}
}
// Usage with full type inference
const client = new ApiClient();
async function loadUserData() {
// TypeScript knows this returns UserResponse
const userResponse = await client.get('user');
console.log(userResponse.data.email); // ✅ Type-safe access
// TypeScript knows this returns TransactionsResponse
const transactionsResponse = await client.get('transactions');
console.log(transactionsResponse.data.pagination.hasNext); // ✅ Type-safe access
}Discriminated Unions for State Management
// Define different loading states
type LoadingState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
// Smart component that handles all states
interface AsyncDataProps<T> {
state: LoadingState<T>;
onRetry?: () => void;
children: (data: T) => React.ReactNode;
}
function AsyncData<T>({ state, onRetry, children }: AsyncDataProps<T>) {
switch (state.status) {
case 'idle':
return <div>Ready to load...</div>;
case 'loading':
return <div className="spinner">Loading...</div>;
case 'success':
// TypeScript knows state.data exists and is of type T
return <>{children(state.data)}</>;
case 'error':
return (
<div className="error">
<p>Error: {state.error}</p>
{onRetry && (
<button onClick={onRetry}>Retry</button>
)}
</div>
);
default:
// TypeScript ensures we handle all cases
const _exhaustive: never = state;
return _exhaustive;
}
}
// Custom hook for async data
function useAsyncData<T>(fetchFn: () => Promise<T>) {
const [state, setState] = useState<LoadingState<T>>({ status: 'idle' });
const execute = useCallback(async () => {
setState({ status: 'loading' });
try {
const data = await fetchFn();
setState({ status: 'success', data });
} catch (error) {
setState({
status: 'error',
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}, [fetchFn]);
return { state, execute };
}
// Usage
function TransactionsPage() {
const { state, execute } = useAsyncData(() =>
fetch('/api/transactions').then(res => res.json())
);
useEffect(() => {
execute();
}, [execute]);
return (
<AsyncData state={state} onRetry={execute}>
{(transactions) => (
<DataTable data={transactions} columns={transactionColumns} />
)}
</AsyncData>
);
}Utility Types Mastery
Component Prop Manipulation
// Extract props from existing components
type ButtonProps = React.ComponentProps<'button'>;
type InputProps = React.ComponentProps<'input'>;
// Create variants with modified props
interface CustomButtonProps extends Omit<ButtonProps, 'className'> {
variant: 'primary' | 'secondary' | 'danger';
size: 'sm' | 'md' | 'lg';
}
function CustomButton({ variant, size, ...buttonProps }: CustomButtonProps) {
const className = `btn btn-${variant} btn-${size}`;
return <button {...buttonProps} className={className} />;
}
// Make certain props required
type RequiredProps<T, K extends keyof T> = T & Required<Pick<T, K>>;
interface BaseModalProps {
title?: string;
onClose?: () => void;
children?: React.ReactNode;
}
// Ensure onClose is always provided
type ModalProps = RequiredProps<BaseModalProps, 'onClose'>;
function Modal({ title, onClose, children }: ModalProps) {
return (
<div className="modal">
<div className="modal-header">
{title && <h2>{title}</h2>}
<button onClick={onClose}>×</button>
</div>
<div className="modal-content">{children}</div>
</div>
);
}
// Create readonly versions for immutable data
type ReadonlyDeep<T> = {
readonly [P in keyof T]: T[P] extends object ? ReadonlyDeep<T[P]> : T[P];
};
interface MutableConfig {
api: {
baseUrl: string;
timeout: number;
retries: number;
};
features: {
darkMode: boolean;
analytics: boolean;
};
}
type AppConfig = ReadonlyDeep<MutableConfig>;
function useAppConfig(): AppConfig {
return {
api: {
baseUrl: 'https://api.example.com',
timeout: 5000,
retries: 3
},
features: {
darkMode: true,
analytics: false
}
};
}Template Literal Types
Type-Safe CSS Classes
// Define allowed CSS class patterns
type Color = 'red' | 'blue' | 'green' | 'yellow';
type Shade = '100' | '200' | '300' | '400' | '500';
type ColorClass = `text-${Color}-${Shade}` | `bg-${Color}-${Shade}`;
type Size = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
type SizeClass = `text-${Size}` | `p-${Size}` | `m-${Size}`;
type AllowedClasses = ColorClass | SizeClass | 'flex' | 'block' | 'hidden';
interface StyledProps {
className?: AllowedClasses | AllowedClasses[];
}
function StyledDiv({ className, children }: StyledProps & { children: React.ReactNode }) {
const classes = Array.isArray(className) ? className.join(' ') : className;
return <div className={classes}>{children}</div>;
}
// Usage - TypeScript will validate these
function Example() {
return (
<>
<StyledDiv className="text-blue-500">Valid class</StyledDiv>
<StyledDiv className={['bg-red-200', 'p-md']}>Multiple valid classes</StyledDiv>
{/* <StyledDiv className="invalid-class">❌ TypeScript error</StyledDiv> */}
</>
);
}
// API endpoint type safety
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type Endpoint = 'users' | 'transactions' | 'accounts';
type ApiEndpoint = `/api/${Endpoint}` | `/api/${Endpoint}/${string}`;
interface ApiRequest<T = any> {
method: HTTPMethod;
endpoint: ApiEndpoint;
data?: T;
}
async function apiCall<T>({ method, endpoint, data }: ApiRequest): Promise<T> {
const response = await fetch(endpoint, {
method,
body: data ? JSON.stringify(data) : undefined,
headers: {
'Content-Type': 'application/json'
}
});
return response.json();
}
// Usage with type-safe endpoints
apiCall({
method: 'GET',
endpoint: '/api/users' // ✅ Valid
});
apiCall({
method: 'POST',
endpoint: '/api/users/123' // ✅ Valid
});
// apiCall({
// method: 'GET',
// endpoint: '/invalid/endpoint' // ❌ TypeScript error
// });Advanced Inference Patterns
Event Handler Type Inference
// Infer event types from element types
type EventFor<T extends keyof HTMLElementTagNameMap> =
T extends 'input' | 'textarea' | 'select'
? React.ChangeEvent<HTMLElementTagNameMap[T]>
: T extends 'form'
? React.FormEvent<HTMLElementTagNameMap[T]>
: T extends 'button'
? React.MouseEvent<HTMLElementTagNameMap[T]>
: React.SyntheticEvent<HTMLElementTagNameMap[T]>;
// Generic form field component
interface FormFieldProps<T extends keyof HTMLElementTagNameMap> {
as: T;
onChange?: (event: EventFor<T>) => void;
[key: string]: any;
}
function FormField<T extends keyof HTMLElementTagNameMap>({
as: Component,
onChange,
...props
}: FormFieldProps<T>) {
return <Component onChange={onChange} {...props} />;
}
// Usage with automatic type inference
function MyForm() {
return (
<div>
<FormField
as="input"
type="text"
onChange={(e) => {
// e is automatically typed as React.ChangeEvent<HTMLInputElement>
console.log(e.target.value);
}}
/>
<FormField
as="button"
onClick={(e) => {
// e is automatically typed as React.MouseEvent<HTMLButtonElement>
console.log('Button clicked');
}}
>
Submit
</FormField>
</div>
);
}
// Function argument type inference
function createAction<T extends Record<string, any>>(
type: string,
payload: T
) {
return { type, payload };
}
// TypeScript infers the payload type
const userAction = createAction('SET_USER', {
id: '123',
name: 'John Doe',
email: 'john@example.com'
});
// userAction.payload is automatically typed as:
// { id: string; name: string; email: string; }Real-World Banking App Example
// Complete type-safe transaction system
type TransactionType = 'transfer' | 'payment' | 'deposit' | 'withdrawal';
type TransactionStatus = 'pending' | 'processing' | 'completed' | 'failed';
interface BaseTransaction {
id: string;
amount: number;
currency: string;
timestamp: Date;
status: TransactionStatus;
description?: string;
}
type TransactionData<T extends TransactionType> =
T extends 'transfer' ? {
fromAccount: string;
toAccount: string;
beneficiary: {
name: string;
bankCode: string;
};
} :
T extends 'payment' ? {
merchant: {
name: string;
category: string;
location?: string;
};
paymentMethod: 'card' | 'digital_wallet';
} :
T extends 'deposit' | 'withdrawal' ? {
atm?: {
id: string;
location: string;
};
branch?: {
id: string;
name: string;
};
} :
never;
type Transaction<T extends TransactionType = TransactionType> =
BaseTransaction & {
type: T;
} & TransactionData<T>;
// Type-safe transaction components
interface TransactionItemProps<T extends TransactionType> {
transaction: Transaction<T>;
onSelect?: (transaction: Transaction<T>) => void;
}
function TransactionItem<T extends TransactionType>({
transaction,
onSelect
}: TransactionItemProps<T>) {
const handleClick = () => onSelect?.(transaction);
return (
<div className="transaction-item" onClick={handleClick}>
<div className="transaction-header">
<span className="amount">
{transaction.currency} {transaction.amount.toFixed(2)}
</span>
<span className={`status status-${transaction.status}`}>
{transaction.status}
</span>
</div>
<div className="transaction-details">
{transaction.type === 'transfer' && (
<p>Transfer to {transaction.beneficiary.name}</p>
)}
{transaction.type === 'payment' && (
<p>Payment at {transaction.merchant.name}</p>
)}
{(transaction.type === 'deposit' || transaction.type === 'withdrawal') && (
<p>
{transaction.type} at {transaction.atm?.location || transaction.branch?.name}
</p>
)}
</div>
</div>
);
}
// Usage with full type safety
function TransactionsList() {
const [transactions] = useState<Transaction[]>([]);
return (
<div>
{transactions.map(transaction => (
<TransactionItem
key={transaction.id}
transaction={transaction}
onSelect={(tx) => {
// tx is properly typed based on transaction.type
if (tx.type === 'transfer') {
console.log('Transfer to:', tx.beneficiary.name);
}
}}
/>
))}
</div>
);
}Performance and Best Practices
Best Practices
- • Use strict TypeScript configuration
- • Prefer type annotations over assertions
- • Leverage discriminated unions for state
- • Use const assertions for immutable data
- • Implement proper error boundaries
- • Type your async operations
Performance Tips
- • Use type-only imports when possible
- • Avoid complex computed types in render
- • Leverage TypeScript's structural typing
- • Use branded types for domain modeling
- • Optimize bundle size with tree shaking
- • Profile build times regularly
Conclusion
Advanced TypeScript patterns transform React development from error-prone to confidence-inspiring. These patterns have proven invaluable in production applications, especially in domains like financial services where reliability is paramount.
Start with generic components, gradually adopt conditional types and utility type manipulation, and always prioritize type safety over convenience. The initial investment in learning these patterns pays dividends in reduced bugs, improved developer experience, and easier refactoring.
Ready for Production-Grade TypeScript?
Need help implementing advanced TypeScript patterns in your React applications? I specialize in building type-safe, scalable applications for complex domains.