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.