
TypeScript Best Practices for Frontend Development
Essential TypeScript patterns and practices that every frontend developer should know for building maintainable applications.
TypeScript has become essential for modern frontend development. Here are the key practices that will make your TypeScript code more maintainable and robust.
Type Safety First
Use Strict Mode
Always enable strict mode in your tsconfig.json
:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
Prefer Type Annotations
Be explicit with your types, especially for function parameters and return values:
// Good
function calculateTotal(price: number, tax: number): number {
return price + (price * tax);
}
// Avoid
function calculateTotal(price, tax) {
return price + (price * tax);
}
Interface Design
Use Interfaces for Object Shapes
Interfaces are perfect for defining object structures:
interface User {
readonly id: string;
name: string;
email: string;
createdAt: Date;
preferences?: UserPreferences;
}
interface UserPreferences {
theme: 'light' | 'dark';
notifications: boolean;
}
Extend Interfaces Wisely
Build upon existing interfaces rather than duplicating:
interface BaseEntity {
id: string;
createdAt: Date;
updatedAt: Date;
}
interface User extends BaseEntity {
name: string;
email: string;
}
interface Post extends BaseEntity {
title: string;
content: string;
authorId: string;
}
Utility Types
Leverage Built-in Utility Types
TypeScript provides powerful utility types:
// Partial - make all properties optional
type CreateUserInput = Partial<User>;
// Pick - select specific properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Omit - exclude specific properties
type CreateUser = Omit<User, 'id' | 'createdAt'>;
// Record - create object type with specific keys
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
Generic Patterns
Create Reusable Generic Types
Generics make your code more flexible:
interface ApiResponse<T> {
data: T;
success: boolean;
message?: string;
}
interface PaginatedResponse<T> extends ApiResponse<T[]> {
pagination: {
page: number;
limit: number;
total: number;
};
}
// Usage
type UsersResponse = PaginatedResponse<User>;
type PostResponse = ApiResponse<Post>;
React with TypeScript
Component Props
Always type your React 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,
size = 'medium',
disabled = false,
onClick,
children
}) => {
return (
<button
className={`btn btn-${variant} btn-${size}`}
disabled={disabled}
onClick={onClick}
>
{children}
</button>
);
};
Hooks with TypeScript
Type your custom hooks properly:
interface UseApiState<T> {
data: T | null;
loading: boolean;
error: string | null;
}
function useApi<T>(url: string): UseApiState<T> {
const [state, setState] = useState<UseApiState<T>>({
data: null,
loading: true,
error: null
});
useEffect(() => {
// API call implementation
}, [url]);
return state;
}
Error Handling
Use Discriminated Unions for Results
Handle success and error states with discriminated unions:
type Result<T, E = Error> =
| { success: true; data: T }
| { success: false; error: E };
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 as Error };
}
}
// Usage
const result = await fetchUser('123');
if (result.success) {
console.log(result.data.name); // TypeScript knows this is User
} else {
console.error(result.error.message); // TypeScript knows this is Error
}
Performance Tips
Use const
Assertions
Preserve literal types with const assertions:
// Without const assertion
const themes = ['light', 'dark']; // Type: string[]
// With const assertion
const themes = ['light', 'dark'] as const; // Type: readonly ['light', 'dark']
type Theme = typeof themes[number]; // Type: 'light' | 'dark'
Lazy Load Types
For large applications, consider lazy loading types:
// types/index.ts
export type { User } from './user';
export type { Post } from './post';
// Use dynamic imports when needed
const getUserModule = () => import('./types/user');
TypeScript is a powerful tool that, when used correctly, can significantly improve your code quality and developer experience. These practices will help you build more maintainable and robust frontend applications.