TypeScript Best Practices for Modern Development
TypeScript Best Practices
TypeScript has become the de facto standard for building large-scale JavaScript applications. Let's explore some best practices that will make your TypeScript code more robust and maintainable.
1. Use Strict Mode
Always enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}2. Leverage Type Inference
TypeScript is smart about inferring types. Don't over-annotate:
// ❌ Unnecessary type annotation
const name: string = "Ankit";
// ✅ Let TypeScript infer
const name = "Ankit";3. Use Interfaces Over Type Aliases (When Possible)
Interfaces are more extensible and provide better error messages:
// ✅ Preferred for objects
interface User {
id: string;
name: string;
email: string;
}
// ✅ Good for unions and primitives
type Status = 'active' | 'inactive' | 'pending';4. Avoid any Type
The any type defeats the purpose of TypeScript. Use unknown instead:
// ❌ Avoid
function process(data: any) {
return data.value;
}
// ✅ Better
function process(data: unknown) {
if (typeof data === 'object' && data !== null && 'value' in data) {
return (data as { value: string }).value;
}
}5. Use Utility Types
TypeScript provides powerful utility types:
interface User {
id: string;
name: string;
email: string;
password: string;
}
// Partial - all properties optional
type UserUpdate = Partial<User>;
// Pick - select specific properties
type UserPublic = Pick<User, 'id' | 'name' | 'email'>;
// Omit - exclude specific properties
type UserSafe = Omit<User, 'password'>;
// Record - create object type
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;6. Use Enums Carefully
Prefer const enums or string literal unions:
// ❌ Regular enum (generates runtime code)
enum Status {
Active,
Inactive
}
// ✅ Const enum (no runtime code)
const enum Status {
Active,
Inactive
}
// ✅ String literal union (my favorite)
type Status = 'active' | 'inactive';7. Generic Constraints
Use constraints to make generics more specific:
// ✅ Constrained generic
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'Ankit', age: 25 };
const name = getProperty(user, 'name'); // Type: string8. Discriminated Unions
Create type-safe state machines:
type LoadingState = {
status: 'loading';
};
type SuccessState<T> = {
status: 'success';
data: T;
};
type ErrorState = {
status: 'error';
error: Error;
};
type State<T> = LoadingState | SuccessState<T> | ErrorState;
function handleState<T>(state: State<T>) {
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
}
}9. Readonly Properties
Make properties immutable:
interface Config {
readonly apiUrl: string;
readonly timeout: number;
}
// Or use Readonly utility type
type Config = Readonly<{
apiUrl: string;
timeout: number;
}>;10. Organize with Namespaces
Keep related types together:
namespace API {
export interface User {
id: string;
name: string;
}
export interface Post {
id: string;
userId: string;
title: string;
}
export type Response<T> = {
data: T;
error?: string;
};
}
// Usage
const user: API.User = { id: '1', name: 'Ankit' };
const response: API.Response<API.User> = { data: user };Conclusion
TypeScript is a powerful tool that, when used correctly, can significantly improve code quality and developer experience. Follow these best practices to write more maintainable, type-safe code.
Key Takeaways:
- Enable strict mode
- Let TypeScript infer types when possible
- Avoid
any, useunknown - Leverage utility types
- Use discriminated unions for complex state
Share this post

About Ankit Chaubey
Full-stack developer passionate about modern web technologies