TypeScript & React Patterns
Comprehensive coding patterns and conventions for TypeScript and React Native development
TypeScript & React Patterns
Overview
This document defines the exact patterns and conventions for TypeScript and React Native development. Every team member and AI assistant must follow these patterns to maintain our "single author" codebase consistency.
Mandatory: These patterns are not suggestions. They are required standards that must be followed in all code.
TypeScript Conventions
Type vs Interface
Rule: Use interface for object shapes, type for unions, intersections, and primitives.
// ✅ GOOD - Interface for objects
interface User {
id: string;
name: string;
email: string;
}
interface ApiResponse<T> {
data: T;
status: number;
message?: string;
}
// ✅ GOOD - Type for unions and primitives
type Status = 'idle' | 'loading' | 'success' | 'error';
type ID = string | number;
type Nullable<T> = T | null;
// ❌ BAD - Using type for simple objects
type User = {
id: string;
name: string;
};Naming Conventions
// Interfaces: PascalCase with descriptive names
interface UserProfile { }
interface ApiClientConfig { }
interface AuthenticationState { }
// Types: PascalCase, descriptive with suffix for clarity
type ButtonVariant = 'primary' | 'secondary';
type ApiMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type UserId = string;
// Enums: PascalCase with UPPER_SNAKE_CASE values
enum UserRole {
ADMIN = 'ADMIN',
USER = 'USER',
GUEST = 'GUEST'
}
// Generic Type Parameters: Single uppercase letters or descriptive
function map<T, R>(items: T[], fn: (item: T) => R): R[] { }
interface ApiResponse<TData, TError = Error> { }Function Types
// ✅ GOOD - Clear function type definitions
interface UserService {
getUser: (id: string) => Promise<User>;
updateUser: (id: string, data: Partial<User>) => Promise<User>;
deleteUser: (id: string) => Promise<void>;
}
// ✅ GOOD - Standalone function types
type Validator<T> = (value: T) => boolean;
type AsyncCallback<T> = (data: T) => Promise<void>;
// ❌ BAD - Unclear or overly complex inline types
interface Service {
fetch: (id: string) => Promise<any>; // Avoid 'any'
update: Function; // Too generic
}Import Organization
Rule: Organize imports in this exact order:
// 1. React and React Native imports
import React, { useState, useEffect } from 'react';
import { View, Text, StyleSheet } from 'react-native';
// 2. Third-party libraries
import { useQuery } from '@tanstack/react-query';
import { z } from 'zod';
// 3. Absolute imports from project (ordered by path depth)
import { useAuth } from '@/core/domains/auth';
import { Button } from '@/ui/foundation/Button';
import { Modal } from '@/ui/patterns/Modal';
import { ProductCard } from '@/ui/business/ProductCard';
import { api } from '@/core/api';
// 4. Relative imports
import { LocalComponent } from './LocalComponent';
import { styles } from './styles';
import type { LocalType } from './types';
// 5. Type imports (if not inline)
import type { User, Product } from '@/core/domains/types';React Component Patterns
Functional Components
Rule: Always use function declarations for components, arrow functions for inline callbacks.
// ✅ GOOD - Function declaration for components
export function UserCard({ user, onPress }: UserCardProps) {
return (
<TouchableOpacity onPress={() => onPress(user.id)}>
<Text>{user.name}</Text>
</TouchableOpacity>
);
}
// ❌ BAD - Arrow function for components
export const UserCard = ({ user, onPress }: UserCardProps) => { };Component Structure
Rule: Follow this exact structure for all components:
// 1. Imports
import React, { useState, useEffect } from 'react';
import { View, Text } from 'react-native';
// 2. Type definitions
interface ProductCardProps {
product: Product;
onPress?: (id: string) => void;
variant?: 'default' | 'compact';
}
// 3. Component definition
export function ProductCard({
product,
onPress,
variant = 'default'
}: ProductCardProps) {
// 4. Hooks (state, context, custom)
const [isLoaded, setIsLoaded] = useState(false);
const { user } = useAuth();
// 5. Derived state / memoized values
const formattedPrice = useMemo(
() => formatCurrency(product.price),
[product.price]
);
// 6. Effects
useEffect(() => {
// Effect logic
}, []);
// 7. Event handlers
const handlePress = useCallback(() => {
onPress?.(product.id);
}, [product.id, onPress]);
// 8. Render helpers (if needed)
const renderPrice = () => {
return <Text>{formattedPrice}</Text>;
};
// 9. Main render
return (
<View style={styles.container}>
{/* Component JSX */}
</View>
);
}
// 10. Styles (if in same file)
const styles = StyleSheet.create({
container: {
// styles
}
});Props Patterns
// ✅ GOOD - Explicit, well-typed props
interface ButtonProps {
title: string;
onPress: () => void;
variant?: 'primary' | 'secondary';
disabled?: boolean;
loading?: boolean;
testID?: string;
}
// ✅ GOOD - Extending native props
interface CustomTextProps extends TextProps {
variant?: 'heading' | 'body' | 'caption';
color?: keyof typeof theme.colors;
}
// ❌ BAD - Unclear or overly permissive props
interface ButtonProps {
[key: string]: any; // Too permissive
onClick: Function; // Too generic
style?: any; // Should be ViewStyle
}Hook Patterns
Custom Hook Structure
// ✅ GOOD - Well-structured custom hook
export function useProductSearch(initialQuery = '') {
// 1. State declarations
const [query, setQuery] = useState(initialQuery);
const [filters, setFilters] = useState<ProductFilters>({});
// 2. Other hooks
const debouncedQuery = useDebounce(query, 300);
// 3. Queries/mutations
const { data, isLoading, error } = useQuery({
queryKey: ['products', 'search', debouncedQuery, filters],
queryFn: () => searchProducts(debouncedQuery, filters),
enabled: debouncedQuery.length > 0,
});
// 4. Callbacks/handlers
const updateFilters = useCallback((newFilters: Partial<ProductFilters>) => {
setFilters(prev => ({ ...prev, ...newFilters }));
}, []);
// 5. Effects
useEffect(() => {
// Reset filters when query changes significantly
if (query.length === 0) {
setFilters({});
}
}, [query]);
// 6. Return object with consistent naming
return {
// State
query,
filters,
products: data?.products ?? [],
// Status
isLoading,
error,
// Actions
setQuery,
updateFilters,
clearFilters: () => setFilters({}),
};
}Hook Naming Conventions
// Data fetching hooks
useUser(id: string) // Single item
useUsers(filters?: Filters) // Multiple items
useCreateUser() // Mutation
useUpdateUser() // Mutation
useDeleteUser() // Mutation
// State management hooks
useCartState() // Local state
useAuthState() // Global state
useFormState<T>() // Generic state
// Utility hooks
useDebounce<T>(value: T, delay: number)
useLocalStorage<T>(key: string, initialValue: T)
usePrevious<T>(value: T)State Management Patterns
Local State
// ✅ GOOD - Clear state management
function ProductList() {
// Group related state
const [filters, setFilters] = useState<ProductFilters>({
category: null,
priceRange: null,
sortBy: 'relevance',
});
// Separate concerns
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [isSelectionMode, setIsSelectionMode] = useState(false);
// Update functions next to state
const toggleSelection = (id: string) => {
setSelectedIds(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
};
}Global State (Zustand)
// ✅ GOOD - Consistent store structure
interface CartStore {
// State
items: CartItem[];
isLoading: boolean;
// Computed
get total(): number;
get itemCount(): number;
// Actions
addItem: (product: Product, quantity: number) => void;
removeItem: (productId: string) => void;
updateQuantity: (productId: string, quantity: number) => void;
clearCart: () => void;
// Async actions
syncWithServer: () => Promise<void>;
}
export const useCartStore = create<CartStore>((set, get) => ({
// Implementation
}));Error Handling Patterns
Try-Catch Patterns
// ✅ GOOD - Consistent error handling
async function updateUserProfile(data: UpdateProfileData) {
try {
// Input validation first
const validated = profileSchema.parse(data);
// Main operation
const response = await api.updateProfile(validated);
// Success handling
showToast({ type: 'success', message: 'Profile updated' });
return response;
} catch (error) {
// Type-safe error handling
if (error instanceof ZodError) {
showToast({ type: 'error', message: 'Invalid data' });
throw new ValidationError('Profile validation failed', error);
}
if (error instanceof ApiError) {
showToast({ type: 'error', message: error.message });
throw error;
}
// Unknown errors
console.error('Unexpected error:', error);
showToast({ type: 'error', message: 'Something went wrong' });
throw new Error('Failed to update profile');
}
}Error Boundaries
// ✅ GOOD - Feature-level error boundary
export function ProductListScreen() {
return (
<ErrorBoundary
fallback={<ErrorFallback onRetry={() => window.location.reload()} />}
onError={(error) => {
logError(error, { screen: 'ProductList' });
}}
>
<ProductList />
</ErrorBoundary>
);
}Performance Patterns
Memoization Rules
// ✅ GOOD - Appropriate memoization
function ProductCard({ product, onPress }: ProductCardProps) {
// Memoize expensive computations
const discountPercentage = useMemo(
() => calculateDiscount(product.price, product.salePrice),
[product.price, product.salePrice]
);
// Memoize callbacks passed to children
const handlePress = useCallback(() => {
onPress(product.id);
}, [product.id, onPress]);
// Don't memoize simple operations
const isOnSale = product.salePrice < product.price; // ✅ Simple comparison
return <View>{/* ... */}</View>;
}
// Component memoization for expensive renders
export const ProductCard = memo(_ProductCard, (prev, next) => {
return prev.product.id === next.product.id &&
prev.product.updatedAt === next.product.updatedAt;
});List Rendering
// ✅ GOOD - Optimized list rendering
function ProductList({ products }: { products: Product[] }) {
const renderItem = useCallback(({ item }: { item: Product }) => (
<ProductCard
key={item.id} // Always include key
product={item}
onPress={handleProductPress}
/>
), [handleProductPress]);
const keyExtractor = useCallback((item: Product) => item.id, []);
const getItemLayout = useCallback((data: any, index: number) => ({
length: ITEM_HEIGHT,
offset: ITEM_HEIGHT * index,
index,
}), []);
return (
<FlatList
data={products}
renderItem={renderItem}
keyExtractor={keyExtractor}
getItemLayout={getItemLayout}
removeClippedSubviews
maxToRenderPerBatch={10}
windowSize={10}
/>
);
}Testing Patterns
Component Testing
// ✅ GOOD - Comprehensive component test
describe('ProductCard', () => {
const mockProduct: Product = {
id: '1',
name: 'Test Product',
price: 99.99,
image: 'https://example.com/image.jpg',
};
it('renders product information correctly', () => {
const { getByText, getByTestId } = render(
<ProductCard product={mockProduct} />
);
expect(getByText('Test Product')).toBeTruthy();
expect(getByText('$99.99')).toBeTruthy();
expect(getByTestId('product-image')).toHaveProperty('source', {
uri: mockProduct.image,
});
});
it('calls onPress with product id when pressed', () => {
const onPress = jest.fn();
const { getByTestId } = render(
<ProductCard product={mockProduct} onPress={onPress} />
);
fireEvent.press(getByTestId('product-card'));
expect(onPress).toHaveBeenCalledWith(mockProduct.id);
});
});Code Comments
When to Comment
// ✅ GOOD - Explains why, not what
function calculateShippingCost(items: CartItem[], address: Address): number {
// Free shipping for orders over $100 (business requirement BR-123)
if (getTotalPrice(items) > 100) {
return 0;
}
// Express shipping surcharge for specific regions due to carrier limitations
const expressSurcharge = EXPRESS_REGIONS.includes(address.state) ? 15 : 0;
return BASE_SHIPPING_COST + expressSurcharge;
}
// ❌ BAD - Obvious comments
function addNumbers(a: number, b: number): number {
// Add a and b together
return a + b; // Return the sum
}Documentation Comments
/**
* Processes a payment using the configured payment provider.
*
* @param payment - Payment details including amount and method
* @param options - Additional processing options
* @returns Payment confirmation or throws PaymentError
*
* @example
* ```typescript
* const confirmation = await processPayment({
* amount: 99.99,
* method: 'credit_card',
* token: 'tok_123'
* });
* ```
*
* @throws {PaymentError} When payment fails or is declined
* @throws {ValidationError} When payment data is invalid
*/
export async function processPayment(
payment: PaymentRequest,
options?: PaymentOptions
): Promise<PaymentConfirmation> {
// Implementation
}Common Anti-Patterns to Avoid
Never do these in our codebase:
// ❌ Using 'any' type
const data: any = fetchData(); // BAD
// ❌ Ignoring TypeScript errors
// @ts-ignore // NEVER DO THIS
const result = someFunction();
// ❌ Inconsistent naming
const user_name = 'John'; // Should be userName
const GetUserData = () => {}; // Should be getUserData
// ❌ Magic numbers/strings
if (status === 2) { } // What is 2?
setTimeout(fn, 3000); // Use constants
// ❌ Nested ternaries
const color = isActive ? 'blue' : isDisabled ? 'gray' : isError ? 'red' : 'black';
// ❌ Large inline functions
<Button onPress={() => {
// 20+ lines of code here
}} />
// ❌ Direct state mutations
state.items.push(newItem); // Mutating state
items[0].name = 'New Name'; // Mutating propsEnforcement Checklist
Before submitting code, verify:
- All TypeScript errors are resolved
- Imports are organized correctly
- Component structure follows the standard
- Props are fully typed (no
any) - Custom hooks start with 'use'
- Error handling is comprehensive
- Performance optimizations are applied where needed
- Code comments explain 'why' not 'what'
- No anti-patterns are present
Quick Reference
// Interface for objects
interface User { name: string; }
// Type for unions/primitives
type Status = 'active' | 'inactive';
// Function components
export function Component() { }
// Custom hooks
export function useCustomHook() { }
// Event handlers
const handleClick = () => { };
// Async functions
const fetchData = async () => { };
// Constants
const MAX_RETRIES = 3;
const API_TIMEOUT = 5000;Related Documents
API Development Decision Matrix
HTTP Method & Status Code Standards
Use this matrix for consistent API integration patterns:
| Scenario | HTTP Method | Status Code | Response Format | Error Handling |
|---|---|---|---|---|
| Resource Creation | POST | 201 | Created resource + location | 400 for validation, 409 for conflicts |
| Resource Retrieval | GET | 200/404 | Full resource or error | 404 with helpful message |
| Resource Update | PUT/PATCH | 200/204 | Updated resource or no content | 400 for validation, 404 if not found |
| Resource Deletion | DELETE | 204 | No content | 404 if not found, 409 if dependencies |
| Batch Operations | POST | 207 | Multi-status response | Individual status per item |
| Search/Filter | GET | 200 | Paginated results | 400 for invalid query params |
| File Upload | POST | 201 | File metadata + URL | 413 for file too large |
API Function Patterns
// ✅ GOOD - Consistent API patterns
// core/domains/products/api.ts
// GET single resource
export const getProduct = async (id: string): Promise<Product> => {
return apiClient.get(`/products/${id}`);
};
// GET collection with filters
export const getProducts = async (filters?: ProductFilters): Promise<PaginatedResponse<Product>> => {
return apiClient.get('/products', { params: filters });
};
// CREATE resource
export const createProduct = async (data: CreateProductDTO): Promise<Product> => {
return apiClient.post('/products', data);
};
// UPDATE resource (full)
export const updateProduct = async (id: string, data: UpdateProductDTO): Promise<Product> => {
return apiClient.put(`/products/${id}`, data);
};
// UPDATE resource (partial)
export const patchProduct = async (id: string, data: Partial<UpdateProductDTO>): Promise<Product> => {
return apiClient.patch(`/products/${id}`, data);
};
// DELETE resource
export const deleteProduct = async (id: string): Promise<void> => {
return apiClient.delete(`/products/${id}`);
};
// BATCH operations
export const batchUpdateProducts = async (updates: BatchUpdate[]): Promise<BatchResponse> => {
return apiClient.post('/products/batch', { operations: updates });
};React Query Integration Patterns
// ✅ GOOD - Consistent hook patterns
// core/domains/products/hooks.ts
// GET hooks
export function useProduct(id: string, options?: UseQueryOptions<Product>) {
return useQuery({
queryKey: productQueryKeys.detail(id),
queryFn: () => getProduct(id),
staleTime: 5 * 60 * 1000, // 5 minutes
...options,
});
}
export function useProducts(filters?: ProductFilters) {
return useQuery({
queryKey: productQueryKeys.list(filters),
queryFn: () => getProducts(filters),
staleTime: 1 * 60 * 1000, // 1 minute for lists
});
}
// CREATE mutation
export function useCreateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: createProduct,
onSuccess: (newProduct) => {
// Invalidate and refetch lists
queryClient.invalidateQueries({
queryKey: productQueryKeys.lists()
});
// Add to cache immediately
queryClient.setQueryData(
productQueryKeys.detail(newProduct.id),
newProduct
);
showToast({ type: 'success', message: 'Product created successfully' });
},
onError: (error: ApiError) => {
if (error.status === 409) {
showToast({ type: 'error', message: 'Product already exists' });
} else {
showToast({ type: 'error', message: error.message });
}
},
});
}
// UPDATE mutation
export function useUpdateProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: ({ id, data }: { id: string; data: UpdateProductDTO }) =>
updateProduct(id, data),
onSuccess: (updatedProduct) => {
// Update cache
queryClient.setQueryData(
productQueryKeys.detail(updatedProduct.id),
updatedProduct
);
// Invalidate lists
queryClient.invalidateQueries({
queryKey: productQueryKeys.lists()
});
},
});
}
// DELETE mutation
export function useDeleteProduct() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: deleteProduct,
onSuccess: (_, deletedId) => {
// Remove from cache
queryClient.removeQueries({
queryKey: productQueryKeys.detail(deletedId),
});
// Invalidate lists
queryClient.invalidateQueries({
queryKey: productQueryKeys.lists()
});
},
});
}Error Response Handling
// ✅ GOOD - Standardized error responses
interface ApiErrorResponse {
error: {
code: string;
message: string;
details?: Record<string, any>;
timestamp: string;
path: string;
};
}
// Error handler in API client
const handleApiError = (error: AxiosError<ApiErrorResponse>): never => {
if (error.response) {
const { status, data } = error.response;
switch (status) {
case 400:
throw new ValidationError(data.error.message, data.error.details);
case 401:
throw new AuthenticationError('Please login to continue');
case 403:
throw new AuthorizationError('You do not have permission');
case 404:
throw new NotFoundError(data.error.message);
case 409:
throw new ConflictError(data.error.message);
case 422:
throw new ValidationError('Invalid data provided', data.error.details);
case 429:
throw new RateLimitError('Too many requests, please try again later');
case 500:
throw new ServerError('Server error, please try again');
default:
throw new ApiError(data.error.message, status);
}
}
if (error.request) {
throw new NetworkError('Network error, please check your connection');
}
throw new UnknownError('An unexpected error occurred');
};