Best Practices & Patterns
Let's explore TypeScript best practices and common patterns that will help you write better, more maintainable code!
Code Organization
Project Structure
src/
├── components/ # React/UI components
├── services/ # Business logic and API calls
├── types/ # Shared type definitions
├── utils/ # Helper functions
└── constants/ # Shared constants
File Naming Conventions
// Component files: PascalCase
UserProfile.tsx
ButtonGroup.tsx
// Utility/service files: camelCase
stringUtils.ts
apiService.ts
// Type definition files
types.ts
user.types.ts
Type Safety Patterns
Type Guards
// User-defined type guards
function isString(value: unknown): value is string {
return typeof value === 'string';
}
function isUser(value: any): value is User {
return value
&& typeof value === 'object'
&& 'id' in value
&& 'name' in value;
}
// Using type guards
if (isString(value)) {
console.log(value.toUpperCase());
}
Discriminated Unions
type Success = {
type: 'success';
data: string;
};
type Error = {
type: 'error';
message: string;
};
type Result = Success | Error;
function handleResult(result: Result) {
switch (result.type) {
case 'success':
return result.data; // TypeScript knows this is string
case 'error':
return result.message; // TypeScript knows this is string
}
}
Error Handling Patterns
Result Types
interface Success<T> {
success: true;
data: T;
}
interface Failure {
success: false;
error: Error;
}
type Result<T> = Success<T> | Failure;
async function fetchUser(id: string): Promise<Result<User>> {
try {
const user = await api.getUser(id);
return { success: true, data: user };
} catch (error) {
return { success: false, error };
}
}
Error Classes
class ApplicationError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number
) {
super(message);
this.name = 'ApplicationError';
}
}
class ValidationError extends ApplicationError {
constructor(message: string) {
super(message, 'VALIDATION_ERROR', 400);
}
}
Code Organization Patterns
Repository Pattern
interface UserRepository {
find(id: string): Promise<User>;
save(user: User): Promise<void>;
delete(id: string): Promise<void>;
}
class PostgresUserRepository implements UserRepository {
async find(id: string): Promise<User> {
// Implementation
}
async save(user: User): Promise<void> {
// Implementation
}
async delete(id: string): Promise<void> {
// Implementation
}
}
Service Pattern
class UserService {
constructor(private userRepo: UserRepository) {}
async createUser(data: CreateUserDTO): Promise<User> {
const user = new User(data);
await this.userRepo.save(user);
return user;
}
}
React Patterns
Prop Types
interface ButtonProps {
variant: 'primary' | 'secondary';
size?: 'small' | 'medium' | 'large';
onClick: () => void;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
variant,
size = 'medium',
onClick,
children
}) => {
// Implementation
};
Custom Hooks
function useLocalStorage<T>(key: string, initialValue: T) {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
return initialValue;
}
});
return [storedValue, setStoredValue] as const;
}
Performance Patterns
Memoization
const memoizedValue = useMemo(() => {
return expensiveComputation(prop1, prop2);
}, [prop1, prop2]);
const memoizedCallback = useCallback(() => {
doSomething(prop1, prop2);
}, [prop1, prop2]);
Lazy Loading
const LazyComponent = lazy(() => import('./LazyComponent'));
function App() {
return (
<Suspense fallback={<Loading />}>
<LazyComponent />
</Suspense>
);
}
Testing Patterns
Test Utilities
// test-utils.ts
type DeepPartial<T> = {
[P in keyof T]?: DeepPartial<T[P]>;
};
function createMockUser(override?: DeepPartial<User>): User {
return {
id: '1',
name: 'Test User',
email: 'test@example.com',
...override
};
}
Test Cases
describe('UserService', () => {
let service: UserService;
let mockRepo: jest.Mocked<UserRepository>;
beforeEach(() => {
mockRepo = {
find: jest.fn(),
save: jest.fn(),
delete: jest.fn()
};
service = new UserService(mockRepo);
});
it('should create user', async () => {
const userData = createMockUser();
await service.createUser(userData);
expect(mockRepo.save).toHaveBeenCalledWith(userData);
});
});
Remember:
- Keep types simple and focused
- Use TypeScript's type system to prevent bugs
- Write self-documenting code with clear types
- Follow consistent naming conventions
- Use patterns that enhance code maintainability
- Test your code thoroughly with proper types