Best Practices

TypeScript gives you a powerful type system, but it is easy to misuse.

Prefer unknown Over any

The any type disables type checking entirely. When you do not know the type of a value, use unknown instead — it forces you to narrow the type before using it:

// Bad: no type safety
function processData(data: any) {
  data.toUpperCase();  // no error — but may crash at runtime
}

// Good: must narrow before use
function processData(data: unknown) {
  if (typeof data === "string") {
    data.toUpperCase();  // safe — TypeScript knows it's a string
  }
}

Enable Strict Mode

The strict flag in tsconfig.json enables a collection of stricter type-checking options. Without it, TypeScript is more lenient, and you lose many of its benefits:

{
  "compilerOptions": {
    "strict": true
  }
}

Among other things, strict enables strictNullChecks, which prevents you from using null or undefined where a value is expected:

// With strictNullChecks enabled:
function getLength(name: string) {
  return name.length;
}

getLength(null);
// Error: Argument of type 'null' is not assignable
//        to parameter of type 'string'

Optional Chaining with ?.

Optional chaining lets you safely access properties on objects that might be null or undefined:

type User = {
  name: string;
  address?: {
    city?: string;
  };
};

function getCity(user: User): string | undefined {
  return user.address?.city;
}

Without optional chaining, you would need verbose null checks:

// Without optional chaining
const city = user.address ? user.address.city : undefined;

Optional chaining works with method calls and array access too:

type Item = { id: number; name: string };
const someArray: Item[] | undefined = [
  { id: 1, name: "Ali" },
  { id: 2, name: "Bob" },
];

const selectedName = someArray?.find((x) => x.id === 1)?.name;

The Non-null Assertion Operator (!)

The ! postfix operator tells TypeScript that a value is not null or undefined, even when TypeScript thinks it might be:

function getElement(): HTMLElement {
  const el = document.getElementById("app")!;  // "Trust me, it exists"
  return el;
}

Let TypeScript Infer

Do not annotate everything. TypeScript’s inference is powerful and usually correct:

// Unnecessary — TypeScript already knows these types
const fullName: string = "Ali";
const numbersExplicit: number[] = [1, 2, 3];
const doubledExplicit: number[] = numbersExplicit.map(
  (n: number): number => n * 2,
);
// Let inference do its job
const fullName = "Ali";
const numbers = [1, 2, 3];
const doubled = numbers.map((n) => n * 2);

Add explicit annotations where inference cannot help — function parameters, documented return types, and variables initialized later:

let result: string; // must annotate — no initial value to infer from
// ...
result = computeResult();

Use as const for Immutable Literals

The as const assertion tells TypeScript to infer the narrowest possible type:

const configWide = {
  api: "https://api.example.com",
  retries: 3,
};
// Type: { api: string; retries: number }

const configNarrow = {
  api: "https://api.example.com",
  retries: 3,
} as const;
// Type: { readonly api: "https://api.example.com"; readonly retries: 3 }

This is especially useful with arrays that should be treated as tuples:

const point = [10, 20] as const;
// Type: readonly [10, 20] — not number[]

Without as const, TypeScript would infer number[] and you’d lose the specific values and the fixed length.