TypeScript Best Practices for 2026
Modern TypeScript patterns and practices to write more maintainable and type-safe code.
TypeScript Best Practices for 2026
TypeScript has become the de facto standard for building JavaScript applications. Let's explore the best practices that will make your TypeScript code more maintainable and robust.
Use Strict Mode
Always enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true
}
}
This catches many potential bugs at compile time.
Type Inference
Let TypeScript infer types when possible:
// Good - type is inferred
const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
// Unnecessary - type annotation not needed
const users: Array<{ name: string; age: number }> = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
];
Avoid any
The any type defeats the purpose of TypeScript:
// Bad
function process(data: any) {
return data.value;
}
// Good
function process(data: { value: string }) {
return data.value;
}
// Better - use generics
function process<T extends { value: string }>(data: T) {
return data.value;
}
Use Type Guards
Create type-safe narrowing with type guards:
interface User {
type: "user";
name: string;
}
interface Admin {
type: "admin";
name: string;
permissions: string[];
}
function isAdmin(user: User | Admin): user is Admin {
return user.type === "admin";
}
function greet(user: User | Admin) {
if (isAdmin(user)) {
// TypeScript knows user is Admin here
console.log(user.permissions);
}
}
Leverage Utility Types
TypeScript provides powerful utility types:
interface User {
id: string;
name: string;
email: string;
password: string;
}
// Only id and name
type UserPreview = Pick<User, "id" | "name">;
// Everything except password
type PublicUser = Omit<User, "password">;
// All fields optional
type PartialUser = Partial<User>;
// All fields readonly
type ImmutableUser = Readonly<User>;
Discriminated Unions
Use discriminated unions for type-safe state management:
type LoadingState =
| { status: "loading" }
| { status: "success"; data: string }
| { status: "error"; error: Error };
function handleState(state: LoadingState) {
switch (state.status) {
case "loading":
return "Loading...";
case "success":
return state.data; // TypeScript knows data exists
case "error":
return state.error.message; // TypeScript knows error exists
}
}
Const Assertions
Use as const for literal types:
// Type: string[]
const colors = ["red", "green", "blue"];
// Type: readonly ["red", "green", "blue"]
const colors = ["red", "green", "blue"] as const;
// Perfect for configuration objects
const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
} as const;
Generic Constraints
Add constraints to generics for better type safety:
// Bad - too generic
function getValue<T>(obj: T, key: string) {
return obj[key]; // Error: T doesn't have index signature
}
// Good - constrained generic
function getValue<T extends Record<string, any>, K extends keyof T>(
obj: T,
key: K
): T[K] {
return obj[key]; // Type-safe!
}
const user = { name: "Alice", age: 30 };
const name = getValue(user, "name"); // Type: string
const age = getValue(user, "age"); // Type: number
Conclusion
These practices will help you write more maintainable TypeScript code. Remember:
- Enable strict mode
- Let TypeScript infer types when possible
- Avoid
anylike the plague - Use type guards and utility types
- Leverage discriminated unions
Happy typing! 🎉