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.

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?