
TypeScript Best Practices for Modern Development
TypeScript has become an essential tool in modern JavaScript development, providing static type checking that catches errors early and improves code quality. Here are some best practices I’ve learned from years of TypeScript development.
1. Use Strict Mode
Always enable strict mode in your tsconfig.json
to catch more potential issues:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
2. Leverage Union Types and Type Guards
Union types provide flexibility while maintaining type safety:
type Status = 'loading' | 'success' | 'error';
function handleStatus(status: Status) {
switch (status) {
case 'loading':
return 'Loading...';
case 'success':
return 'Success!';
case 'error':
return 'Error occurred';
default:
// TypeScript will ensure this is never reached
const exhaustiveCheck: never = status;
return exhaustiveCheck;
}
}
3. Use Generic Types Effectively
Generics make your code reusable while preserving type information:
interface ApiResponse<T> {
data: T;
status: number;
message: string;
}
async function fetchData<T>(url: string): Promise<ApiResponse<T>> {
const response = await fetch(url);
return response.json();
}
// Usage with proper typing
const userResponse = await fetchData<User>('/api/users/1');
// userResponse.data is typed as User
4. Utility Types Are Your Friend
TypeScript provides many utility types that can save you time:
interface User {
id: number;
name: string;
email: string;
createdAt: Date;
}
// Create a type for updating users (all fields optional)
type UpdateUser = Partial<User>;
// Create a type for user creation (omit id and createdAt)
type CreateUser = Omit<User, 'id' | 'createdAt'>;
// Pick only specific fields
type UserSummary = Pick<User, 'id' | 'name'>;
5. Prefer Interfaces Over Types for Object Shapes
While both interfaces and types can define object shapes, interfaces are generally preferred:
// ✅ Good - use interface for object shapes
interface User {
id: number;
name: string;
}
// ✅ Good - interfaces can be extended
interface AdminUser extends User {
permissions: string[];
}
// ✅ Good - use type for unions, primitives, and computed types
type Theme = 'light' | 'dark';
type EventHandler<T> = (event: T) => void;
6. Use Branded Types for Better Type Safety
Branded types help prevent mixing up similar primitive types:
type UserId = string & { readonly brand: unique symbol };
type Email = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function createEmail(email: string): Email {
if (!email.includes('@')) {
throw new Error('Invalid email');
}
return email as Email;
}
// This prevents accidentally mixing up user IDs and emails
function getUser(userId: UserId): User {
// Implementation
}
const id = createUserId('123');
const email = createEmail('user@example.com');
getUser(id); // ✅ Works
getUser(email); // ❌ TypeScript error
7. Async/Await with Proper Error Handling
Use proper typing for async operations:
type Result<T, E = Error> = {
success: true;
data: T;
} | {
success: false;
error: E;
};
async function safeApiCall<T>(
apiCall: () => Promise<T>
): Promise<Result<T>> {
try {
const data = await apiCall();
return { success: true, data };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error : new Error('Unknown error')
};
}
}
// Usage
const result = await safeApiCall(() => fetchUser(userId));
if (result.success) {
console.log(result.data); // Typed as User
} else {
console.error(result.error); // Typed as Error
}
Conclusion
These TypeScript best practices will help you write more maintainable, type-safe code. Remember that TypeScript is a tool to help you catch errors early and improve your development experience. Don’t fight the type system—embrace it and let it guide you toward better code architecture.
The key is to start simple and gradually adopt more advanced patterns as your understanding grows. TypeScript’s power lies not just in catching errors, but in making your code more self-documenting and easier to refactor.