Skip to main content

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:

  1. Keep types simple and focused
  2. Use TypeScript's type system to prevent bugs
  3. Write self-documenting code with clear types
  4. Follow consistent naming conventions
  5. Use patterns that enhance code maintainability
  6. Test your code thoroughly with proper types