TypeScript Best Practices

Good TypeScript is less about clever types and more about a handful of habits - strict settings, honest naming, and narrowing instead of casting - applied consistently.

#Jobseeker

Find matching jobs & Auto apply with AI

Hyring
Jobseeker using Hyring to search, apply, and prepare with AI

Naming Conventions

Use PascalCase for types, interfaces, and classes, and camelCase for variables and functions. Prefix boolean-returning values and properties with `is`, `has`, or `should` so their meaning is obvious at the call site without needing to check the type.

Consistent Naming

// Good: PascalCase for types, camelCase for values, is/has prefix for booleans
interface UserProfile {
  displayName: string;
  isVerified: boolean;
}

function getDisplayName(profile: UserProfile): string {
  return profile.displayName;
}

const profile: UserProfile = { displayName: "Ada", isVerified: true };
console.log(getDisplayName(profile));

Turn On strict Mode

Setting `"strict": true` in `tsconfig.json` enables a bundle of checks - including `strictNullChecks` and `noImplicitAny` - that catch entire categories of bugs, like forgetting that a value can be `null`, at compile time instead of in production.

strictNullChecks in Action

// tsconfig.json: { "compilerOptions": { "strict": true } }
function greet(name: string | null): string {
  if (name === null) {
    return "Hello, guest";
  }
  return `Hello, ${name.toUpperCase()}`;
}

console.log(greet(null));
console.log(greet("ada"));

Avoid any, Reach for unknown

`any` disables type checking entirely for a value, which lets bugs sneak through silently. `unknown` is the type-safe alternative for genuinely unknown input - it forces a narrowing check, such as a `typeof` or `instanceof` test, before you're allowed to use the value for anything.

unknown Instead of any

function parseInput(input: unknown): number {
  if (typeof input === "string" && input.trim() !== "") {
    return Number(input);
  }
  if (typeof input === "number") {
    return input;
  }
  throw new TypeError("Expected a string or number");
}

console.log(parseInput("42"));
console.log(parseInput(7));
  • Use readonly arrays and as const for data that should never mutate
  • Model state with discriminated unions instead of loose boolean flags
  • Write exhaustive switch statements with a never-typed default case
  • Avoid the non-null assertion (!) in favor of real narrowing
  • Keep functions small and single-purpose so their types stay simple
  • Colocate types with the code that uses them instead of one giant types file
PracticeWhy it mattersExample
Enable strictCatches null/undefined bugs at compile timestrict: true
Avoid anyPreserves type safety end-to-enduse unknown + narrowing
Exhaustive switchesCompiler flags new union members you forgot to handledefault: assertNever(x)
Immutable dataPrevents accidental mutation bugsreadonly, as const
Note: Wire up @typescript-eslint/no-explicit-any and @typescript-eslint/no-floating-promises in CI - they turn these best practices into enforced rules instead of things reviewers have to remember to check for.
Note: The non-null assertion operator (!) tells the compiler to trust you without any runtime check - reaching for it out of convenience instead of fixing the underlying narrowing is one of the most common ways strict-mode safety gets quietly undone.

Exercise: TypeScript Best Practices

Why is unknown generally preferred over any for values whose type isn't known yet?