Introduction
TypeScript has become the go-to language for building scalable web applications. Its static typing system helps catch errors early, improves code maintainability, and enhances developer productivity. However, to truly harness TypeScript’s power, you need to follow best practices that go beyond basic type annotations.
Essential TypeScript Patterns
1. Use Strict Configuration
Always enable strict mode in your tsconfig.json
:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
2. Prefer Interfaces Over Types for Object Shapes
// ✅ Good - Use interfaces for object shapes
interface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// ❌ Avoid - Type aliases for simple objects
type User = {
id: string;
name: string;
email: string;
createdAt: Date;
};
3. Leverage Union Types and Type Guards
type LoadingState = 'idle' | 'loading' | 'success' | 'error';
interface DataState {
status: LoadingState;
data?: unknown;
error?: string;
}
function isLoading(state: DataState): state is DataState & { status: 'loading' } {
return state.status === 'loading';
}
function isSuccess(state: DataState): state is DataState & { status: 'success'; data: unknown } {
return state.status === 'success';
}
Advanced Type Patterns
Utility Types
TypeScript provides powerful utility types that can transform existing types:
interface User {
id: string;
name: string;
email: string;
password: string;
createdAt: Date;
}
// Pick specific properties
type UserProfile = Pick<User, 'id' | 'name' | 'email'>;
// Exclude specific properties
type UserWithoutPassword = Omit<User, 'password'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
Generic Constraints
interface Identifiable {
id: string;
}
function updateEntity<T extends Identifiable>(
entity: T,
updates: Partial<T>
): T {
return { ...entity, ...updates };
}
// Usage
const user = { id: '1', name: 'John', email: 'john@example.com' };
const updatedUser = updateEntity(user, { name: 'Jane' });
Error Handling Patterns
Result Type Pattern
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
async function fetchUser(id: string): Promise<Result<User, string>> {
try {
const user = await api.getUser(id);
return { success: true, data: user };
} catch (error) {
return { success: false, error: error.message };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is safe
} else {
console.error(result.error); // Handle error case
}
Custom Error Types
abstract class AppError extends Error {
abstract readonly statusCode: number;
abstract readonly isOperational: boolean;
}
class ValidationError extends AppError {
statusCode = 400;
isOperational = true;
constructor(message: string) {
super(message);
this.name = 'ValidationError';
}
}
class NotFoundError extends AppError {
statusCode = 404;
isOperational = true;
constructor(resource: string) {
super(`${resource} not found`);
this.name = 'NotFoundError';
}
}
React-Specific Best Practices
Component Props
interface ButtonProps {
variant?: 'primary' | 'secondary' | 'danger';
size?: 'small' | 'medium' | 'large';
disabled?: boolean;
onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({
variant = 'primary',
size = 'medium',
disabled = false,
onClick,
children,
}) => {
// Component implementation
};
Custom Hooks
interface UseApiResult<T> {
data: T | null;
loading: boolean;
error: string | null;
refetch: () => void;
}
function useApi<T>(url: string): UseApiResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<string | null>(null);
const fetchData = useCallback(async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
const result = await response.json();
setData(result);
} catch (err) {
setError(err instanceof Error ? err.message : 'An error occurred');
} finally {
setLoading(false);
}
}, [url]);
useEffect(() => {
fetchData();
}, [fetchData]);
return { data, loading, error, refetch: fetchData };
}
Testing with TypeScript
Type-Safe Test Utilities
interface TestUser {
id: string;
name: string;
email: string;
}
function createTestUser(overrides: Partial<TestUser> = {}): TestUser {
return {
id: '1',
name: 'Test User',
email: 'test@example.com',
...overrides,
};
}
// Usage in tests
const user = createTestUser({ name: 'John Doe' });
Performance Considerations
Lazy Loading Types
// Instead of importing all types at once
import type { UserProfile, UserSettings, UserPreferences } from './types';
// Use dynamic imports for large type definitions
type LazyUserTypes = typeof import('./types');
Type-Only Imports
// Use type-only imports when you only need the type
import type { User } from './types';
import { validateUser } from './utils';
function processUser(user: User): void {
if (validateUser(user)) {
// Process user
}
}
Common Pitfalls to Avoid
1. Using any
Type
// ❌ Avoid
function processData(data: any): any {
return data.someProperty;
}
// ✅ Better
function processData<T>(data: T): T extends { someProperty: infer P } ? P : never {
return (data as any).someProperty;
}
2. Overusing Union Types
// ❌ Too complex
type ComplexUnion = string | number | boolean | null | undefined | object;
// ✅ More specific
type Status = 'pending' | 'approved' | 'rejected';
type DataType = string | number;
Conclusion
TypeScript best practices evolve with the language and ecosystem. The key is to leverage TypeScript’s type system to catch errors early, improve code maintainability, and enhance developer experience. By following these patterns and practices, you’ll write more robust, scalable, and maintainable code.
Remember: TypeScript is a tool to help you write better JavaScript. Use it wisely, and it will significantly improve your development experience and code quality.