TypeScript best practices for type safety, strict typing, and maintainability. Use when writing TypeScript code, defining types and interfaces, handling nulls, or when user asks about "type safety", "discriminated unions", "utility types", or "strict TypeScript".
Write type-safe, maintainable TypeScript code.
strict: true in tsconfig.jsonunknown instead of any when type is truly unknownany (use unknown and narrow instead)@ts-ignore or @ts-expect-error without commentas unless absolutely necessary// ❌ Bad
function parse(data: any) {
return data.value; // No type safety
}
// ✅ Good
function parse(data: unknown): string {
if (typeof data === "object" && data !== null && "value" in data) {
const value = (data as { value: unknown }).value;
if (typeof value === "string") {
return value;
}
}
throw new Error("Invalid data format");
}
// ✅ Better - use type guard
function isValidData(data: unknown): data is { value: string } {
return (
typeof data === "object" &&
data !== null &&
"value" in data &&
typeof (data as { value: unknown }).value === "string"
);
}
function parse(data: unknown): string {
if (isValidData(data)) {
return data.value; // TypeScript knows data.value is string
}
throw new Error("Invalid data format");
}
// ❌ Bad - ambiguous state
interface ApiResponse {
data?: User;
error?: Error;
loading?: boolean;
}
// ✅ Good - discriminated union
type ApiResponse =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: User }
| { status: "error"; error: Error };
function handleResponse(response: ApiResponse) {
switch (response.status) {
case "idle":
return "Ready";
case "loading":
return "Loading...";
case "success":
return response.data.name; // TypeScript knows data exists
case "error":
return response.error.message; // TypeScript knows error exists
}
}
// Exhaustive checking helper
function assertNever(x: never): never {
throw new Error(`Unexpected value: ${x}`);
}
Partial, Pick, Omit, Record)readonly for immutable datainterface User {
id: string;
name: string;
email: string;
createdAt: Date;
}
// Partial for updates (all optional)
type UserUpdate = Partial<Omit<User, "id" | "createdAt">>;
// Pick for specific fields
type UserPreview = Pick<User, "id" | "name">;
// Readonly for immutable data
type ReadonlyUser = Readonly<User>;
// Record for dictionaries
type UserById = Record<string, User>;
// Custom utility type
type Nullable<T> = T | null;
type AsyncResult<T> = Promise<{ data: T } | { error: Error }>;
strictNullChecks?.) and nullish coalescing (??)T | null)!) without good reason|| for defaults (use ?? instead)// ❌ Bad
function getName(user: User) {
return user.profile!.name; // Dangerous assertion
}
const value = input || "default"; // Bug: 0 and '' become 'default'
// ✅ Good
function getName(user: User): string | undefined {
return user.profile?.name;
}
const value = input ?? "default"; // Only null/undefined trigger default
// Handle null explicitly
function processUser(user: User | null) {
if (!user) {
return handleNoUser();
}
// TypeScript knows user is not null here
return process(user);
}
TItem, TKey, TValue)// ✅ Simple generic
function first<T>(items: T[]): T | undefined {
return items[0];
}
// ✅ Constrained generic
function getProperty<TObj, TKey extends keyof TObj>(
obj: TObj,
key: TKey,
): TObj[TKey] {
return obj[key];
}
// ✅ Generic with default
interface PaginatedResult<TItem = unknown> {
items: TItem[];
total: number;
page: number;
}
// ✅ Generic utility function
function groupBy<TItem, TKey extends string | number>(
items: TItem[],
keyFn: (item: TItem) => TKey,
): Record<TKey, TItem[]> {
return items.reduce(
(acc, item) => {
const key = keyFn(item);
acc[key] = acc[key] || [];
acc[key].push(item);
return acc;
},
{} as Record<TKey, TItem[]>,
);
}
const objects or union types over enums// ❌ Avoid - numeric enum pitfalls
enum Status {
Active, // 0
Inactive, // 1
}
// ✅ Better - const object
const Status = {
Active: "active",
Inactive: "inactive",
} as const;
type Status = (typeof Status)[keyof typeof Status];
// ✅ Also good - union type
type Status = "active" | "inactive";
// ✅ When enum is appropriate - need reverse mapping
enum HttpStatus {
OK = 200,
NotFound = 404,
}
const statusName = HttpStatus[200]; // 'OK'
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}