TypeScriptJavaScriptBest Practices6 min readFebruary 18, 2024

TypeScript Tips Every Developer Should Know

Practical TypeScript patterns I use in every project — from strict type narrowing to utility types that eliminate boilerplate.

After writing TypeScript daily for two years across frontend, backend, and full-stack projects, these are the patterns that genuinely improve my code — not just make it look type-safe.

1. Discriminated Unions Beat Optional Properties

The moment you have optional properties that depend on each other, reach for discriminated unions.

// ❌ Hard to reason about
type Response = {
  success: boolean;
  data?: User;
  error?: string;
};
 
// ✅ TypeScript narrows this automatically
type Response =
  | { success: true; data: User }
  | { success: false; error: string };
 
function handle(res: Response) {
  if (res.success) {
    console.log(res.data.name); // TypeScript knows data exists
  } else {
    console.log(res.error); // TypeScript knows error exists
  }
}

2. satisfies for Config Objects

The satisfies keyword (TypeScript 4.9+) lets you validate a value against a type while keeping the most specific inferred type.

const palette = {
  red: [255, 0, 0],
  green: "#00ff00",
} satisfies Record<string, string | number[]>;
 
// TypeScript knows palette.red is number[], not string | number[]
palette.red.map(v => v * 2); // ✅ Works

3. Template Literal Types for String APIs

Perfect for building type-safe event systems or API route validators.

type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";
type Route = "/users" | "/posts" | "/comments";
type Endpoint = `${HTTPMethod} ${Route}`;
 
// Valid: "GET /users", "POST /posts"
// Invalid: "PATCH /users", "GET /invalid"
function request(endpoint: Endpoint) { /* ... */ }

4. Utility Types You Should Use Daily

// Pick specific fields
type UserPreview = Pick<User, "id" | "name" | "avatar">;
 
// Make everything optional (great for PATCH endpoints)
type UpdateUser = Partial<User>;
 
// Make everything required (great for validated forms)
type RequiredUser = Required<User>;
 
// Strip null and undefined
type NonNullUser = NonNullable<User | null | undefined>;
 
// Return type of a function
type FetchResult = Awaited<ReturnType<typeof fetchUser>>;

5. Strict Mode Is Non-Negotiable

In tsconfig.json, always enable strict mode:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true
  }
}

noUncheckedIndexedAccess is particularly underrated — it forces you to handle the case where an array index is out of bounds.

6. Type Guards Over Type Assertions

// ❌ Lying to TypeScript
const user = data as User;
 
// ✅ Proving it to TypeScript
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "name" in value
  );
}
 
if (isUser(data)) {
  console.log(data.name); // safely narrowed
}

Conclusion

TypeScript's value isn't just catching bugs — it's making your code self-documenting and refactorable. These patterns will save you hours of debugging and make your APIs a joy to consume.

Start with strict mode, embrace discriminated unions, and let the compiler guide you.

Back to all posts