Mastering TypeScript: Best Practices You Should Start Using Today
⚡ TL;DR:
Don’t just “use TypeScript” — use it smartly.
Follow these practical, real-world patterns:
- Prefer types over
any. - Organize interfaces.
- Leverage generics and utility types.
- Use strict mode.
- Keep your types close to logic.
Code examples inside. 👇
Let’s skip the fluff. I’ve been building with TypeScript for over a decade — from side projects to production-scale SaaS systems. Here’s what actually matters if you want to go from “just using TS” to writing code others love to maintain.
🧩 1. Never Use any Unless You Really Mean “Anything”
You’ll see this a lot in beginner code:
function getUser(id: any) {
return fetch(`/users/${id}`).then(r => r.json());
}
This “works”, but defeats the purpose of TypeScript.
Instead, define what you expect:
function getUser(id: string | number) {
return fetch(`/users/${id}`).then(r => r.json() as Promise<User>);
}
Or even better — use generics:
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json() as T;
}
const user = await fetchJson<User>('/api/user/1');
💡 Pro Tip:
any→ avoid.unknown→ safer fallback if you must accept “anything”.
🗂️ 2. Use type for Composition, interface for Contracts
Both seem similar but have different strengths:
type Base = { id: number }
interface User extends Base { name: string }
But for unions or mapped types:
type Status = 'active' | 'inactive';
type ApiResponse<T> = { data: T; status: Status };
🧠 Rule of thumb:
interface= extending, object shapetype= combining, unions, utilities
⚙️ 3. Turn on Strict Mode (and Leave It On)
In your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
} Strict mode feels painful early on — but it saves you from runtime bugs later.
Example:
function greet(name?: string) {
console.log(`Hello ${name.toUpperCase()}`); // ❌ Error under strictNullChecks
}
Without strict mode, this crashes in production.
With strict mode, TypeScript protects you.
🧱 4. Model Your Data, Don’t Just Type It
If your project uses APIs, model the shape once — reuse everywhere.
Bad:
const user = { id: 1, name: "Sourav", email: "me@dev.com" };
Better:
interface User {
id: number;
name: string;
email: string;
}
const user: User = { id: 1, name: "Sourav", email: "me@dev.com" };
Even better:
type UserPreview = Pick<User, 'id' | 'name'>;
type UserResponse = Omit<User, 'email'>;
🚀 You’ll thank yourself when your app grows and refactors become trivial.
🧩 5. Use Utility Types Instead of Redefining
TypeScript ships with powerful built-ins like Partial, Pick, Omit, Record, and Readonly.
Example:
type Config = {
host: string;
port: number;
secure: boolean;
};
type OptionalConfig = Partial<Config>;
type PublicConfig = Pick<Config, 'host' | 'port'>;
These utilities make your types DRY and scalable.
🧠 6. Write Functions That Infer Types Naturally
Don’t annotate everything manually. Let TypeScript infer.
const multiply = (a: number, b: number) => a * b;
No need to write:
const multiply: (a: number, b: number) => number = (a, b) => a * b;
🧩 Let TS infer where it can, annotate only where it helps readability.
🧰 7. Use Enums Sparingly — Literal Unions Are Better
Instead of:
enum Role {
ADMIN,
USER,
GUEST
}
Prefer:
type Role = 'admin' | 'user' | 'guest';
It’s simpler, easier to debug, and more tree-shakable in modern bundlers.
📁 8. Organize Types Alongside Features
Don’t dump all types in a types.ts file.
Instead, colocate them:
/src
┣ /users
┃ ┣ index.ts
┃ ┣ user.types.ts
┣ /auth
┃ ┣ auth.types.ts
This keeps things maintainable as your project grows.
🧬 9. Prefer as const for Literal Inference
const roles = ['admin', 'user', 'guest'] as const;
type Role = typeof roles[number];
No more manually typing out all string literals. Clean and safe.
🧯 10. Never Ignore Type Errors — Fix or Type Narrow
Don’t “fix” by forcing types:
const data = getData() as any; // ❌ Instead, narrow them:
if ('user' in data) {
// ✅ Safe access
}
Or validate:
function isUser(obj: unknown): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
'id' in obj &&
'email' in obj
);
}
This way, TypeScript remains your ally, not your obstacle.
🧠 Final Advice:
TypeScript isn’t about “adding types” — it’s about communicating intent.
The more clearly your code tells a story, the less your team argues about bugs.
Keep writing, keep refactoring, and remember:
👉 Good TypeScript code reads like good documentation.
