This content originally appeared on Level Up Coding – Medium and was authored by Vivek Garg

When people first start using TypeScript, they typically concentrate on the big, striking features, such as generics, interfaces, and types. However, the true secret to developing clean, manageable TypeScript code is found in the small everyday practices — that most developers neglect when deadlines are approaching and “just make it work” becomes the guiding principle.
So, in this post, let’s talk about 10 underrated best practices in TypeScript that you probably forget in your daily coding. I’ll keep things simple and back it up with code examples. Let’s dive in
1. Prefer unknown Over any 
We all love the “just make it work” button called any. But it’s like duct-taping a leaky pipe—it’ll work until your entire kitchen is flooded. Instead, use unknown. Wanna explore unknown, hit
Instead of blindly trusting any, use unknown. It forces you to do type checks before using the value.
// ❌ Scary and unsafe
function dangerousParse(data: any) {
console.log(data.foo.bar); // RIP if `data` is not what you expect
}
// ✅ Safe and robust
function safeParse(data: unknown) {
// You MUST check the shape first!
if (typeof data === 'object' && data !== null && 'foo' in data) {
// Now TypeScript knows 'foo' exists. We've narrowed the type!
console.log(data.foo);
}
}
Why?
- any disables type checking.
- unknown forces you to perform type checks before using the value.
- It’s safer and prevents runtime surprises.
2. Use never to Handle Impossible States 
Sometimes you reach a code path that should never happen. Instead of ignoring it, let TypeScript help you. Most people ignore never, but it’s a superpower for exhaustive checks.
type Shape = "circle" | "square";
function getArea(shape: Shape) {
switch (shape) {
case "circle":
return 3.14;
case "square":
return 4;
default:
const exhaustiveCheck: never = shape; // 🚨 Error if new shape added but not handled
return exhaustiveCheck;
}
}
This ensures that when someone adds a "triangle", TypeScript screams at you until you handle it.
3. Don’t Forget readonly 
Ever had a variable that should never change… but someone accidentally reassigns it? Enter readonly.
interface Config {
readonly apiUrl: string;
}
const config: Config = { apiUrl: "https://api.example.com" };
// ❌ Error if someone tries:
config.apiUrl = "https://hack.me";
This ensures your objects behave more predictably — no surprise changes later.
4. The null vs undefined Circus 
This is the big one. Most people treat null and undefined as interchangeable "nothing" values. But understanding their intent is crucial for writing predictable code.
- undefined typically means "this hasn't been given a value yet." It's what JavaScript gives you by default.
– A variable declared but not initialized? undefined.
– A missing object property? undefined.
– A function with no explicit return? undefined. - null is an explicit, intentional "empty" value. You, the developer, have to assign it. It means "I am deliberately putting nothing here."
// ❌ The confusing way
interface User {
name: string;
avatar: string | null | undefined; // What does each one mean?!
}
// ✅ The clear, intentional way
interface User {
name: string;
avatar: string | null; // 'null' means the user explicitly removed their avatar.
}
// 'undefined' would mean the user hasn't made a decision yet (e.g., hasn't uploaded one).
// This distinction is powerful for UI logic!
const user1 = { name: "Alice", avatar: null }; // "Show a placeholder icon"
const user2 = { name: "Bob" }; // avatar is undefined -> "Show 'Upload Avatar' button"
Best Practice: Be intentional. Use undefined for things that are not yet set (often handled with optional properties avatar?: string). Use null when you need to explicitly represent the concept of "empty" or "nothing."
5. The Power of Type Guards (Beyond typeof)
You know about typeof and instanceof. But have you met their powerful cousin, user-defined type guards? They let you create custom functions that narrow types in incredibly smart ways.
interface Cat { meow: () => void; }
interface Dog { bark: () => void; }
// 🧙♂ This magic function tells TypeScript what's what.
function isCat(animal: Cat | Dog): animal is Cat {
// The "animal is Cat" is the type predicate. This is the key!
return (animal as Cat).meow !== undefined;
}
const animal: Cat | Dog = getAnimalFromSomewhere();
if (isCat(animal)) {
animal.meow(); // TypeScript KNOWS it's a Cat inside this block. Magic!
} else {
animal.bark(); // And therefore, it must be a Dog here.
}
6. Avoid Non-Null Assertions (!) 
The exclamation mark is like telling TypeScript: “Trust me bro, it’ll never be null.” But reality? It probably will
// Risky ❌
const username: string | null = getUsername();
console.log(username!.toUpperCase()); // Crash incoming 💥
// Safer ✅
if (username) {
console.log(username.toUpperCase());
}
Instead of silencing the compiler, embrace null-safety with checks or optional chaining (?.).
7. Prefer Interfaces for Object Shapes 
Both type and interface exist. But when describing object shapes, interfaces are extendable and cleaner.
interface User {
id: number;
name: string;
}
interface Admin extends User {
role: "admin";
}
I know, type works too, but interfaces scale better when multiple devs are extending stuff.
8. Leverage Partial, Pick keywords
Instead of reinventing the wheel, TypeScript gives you utility types to save time.
type User = {
id: number;
name: string;
email: string;
};
// Partial: makes all optional
const draft: Partial<User> = { name: "John" };
// Pick: selects specific keys
const profile: Pick<User, "name" | "email"> = { name: "John", email: "j@example.com" };
This way typescript will not yell for the missing properties
9. Prefer type + Union Over enum
Enums are fine, but often overused. In many cases, union types are cleaner, simpler, and tree-shakable
type Status = "pending" | "approved" | "rejected";
function updateStatus(status: Status) {
console.log("Status:", status);
}
updateStatus("approved"); // ✅
updateStatus("done"); // ❌ Error
Lightweight, readable, and doesn’t bring runtime overhead.
10. Harness keyof and typeof for DRY Code
Stop repeating yourself! Use keyof and typeof to create types directly from your object structures.
// Define your single source of truth (an object)
const SUPPORTED_LANGUAGES = {
en: "English",
es: "Spanish",
fr: "French",
de: "German",
// jp: "Japanese" // Add this later, and everything updates!
} as const; // `as const` makes the values literal types ('English', not string)
// Derive types from it
type LanguageCode = keyof typeof SUPPORTED_LANGUAGES; // 'en' | 'es' | 'fr' | 'de'
type LanguageName = typeof SUPPORTED_LANGUAGES[LanguageCode]; // "English" | "Spanish" | "French" | "German"
// Use them in your application
function getLanguageName(code: LanguageCode): LanguageName {
return SUPPORTED_LANGUAGES[code];
}
function isLanguageSupported(code: string): code is LanguageCode {
return code in SUPPORTED_LANGUAGES;
}
// ✅ All safe and derived from one object!
const name = getLanguageName('es'); // ✅ Returns "Spanish"
const name = getLanguageName('jp'); // ❌ Compile Error: even before we add it to the object
Above code clearly help you to scale you code keeping single source of truth.
Final words
TypeScript is already a safety net, but using it carelessly is like wearing a seatbelt without buckling it — it looks safe but won’t help when it matters.
By following these 10 best practices — from using unknown instead of any to embracing readOnly and leveraging type guards—you’ll write code that’s not just working but also robust, maintainable, and future-proof.
So, next time you’re tempted to slap an any or skip strict mode, remember: future-you is watching . Write code today that won’t embarrass you tomorrow.
10 TypeScript Best Practices Developers Often Miss (With Examples) was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding – Medium and was authored by Vivek Garg