Embrace Strictness
Enable strict mode to unlock TypeScript's full potential. Strictness highlights implicit any types, nullable values, and type inference gaps before they reach production.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true
}
}
Model Data with Interfaces and Types
Prefer modeling data with dedicated type aliases or interfaces instead of ad-hoc objects. This keeps your domain language consistent across the codebase.
type InvoiceStatus = "draft" | "sent" | "paid";
interface Invoice {
id: string;
amount: number;
status: InvoiceStatus;
dueDate?: Date;
}
Leverage Generics Thoughtfully
Generics help construct reusable components without losing type information. When composing functions, pass type parameters explicitly if inference fails.
function withRetry<T>(operation: () => Promise<T>, attempts = 3): Promise<T> {
return operation().catch((error) => {
if (attempts <= 1) throw error;
return withRetry(operation, attempts - 1);
});
}
Narrow Types Safely
Use user-defined type guards or assertion functions to narrow uncertain values. Type guards keep runtime checks and static guarantees in sync.
interface ApiError {
message: string;
status: number;
}
function isApiError(error: unknown): error is ApiError {
return (
typeof error === "object" &&
error !== null &&
"message" in error &&
"status" in error
);
}
Prefer Composition over Enums
Union types often outshine enums thanks to better tree-shaking and pattern matching.
type Theme = "light" | "dark" | "system";
function applyTheme(theme: Theme) {
// ...
}
Conclusion
By combining strict compiler options, descriptive types, and thoughtful generics, TypeScript becomes a powerful ally in building maintainable web applications. Keep iterating on your type models as the product evolves.
