TypeScript Best Practices :10 Proven Ways

After spending years developing applications with TypeScript, I’ve encountered numerous challenges and learned valuable lessons along the way. TypeScript has transformed how I write JavaScript, making my code more robust and easier to maintain.

In this article, I’ll share the TypeScript best practices that have significantly improved my TypeScript projects. These are techniques I use daily that have saved me countless hours of debugging and refactoring.

Whether you’re new to TypeScript or have been using it for a while, these practices will help you write cleaner, more maintainable code.

Best Practices to Follow in TypeScript Code Structure.

Below are the 10 best practices that we must follow while designing the TypeScript code.

1. Use Strict Compiler Options

One of the first things I do in any TypeScript project is to enable strict type checking. This has consistently prevented subtle bugs from sneaking into production.

Here’s how I configure my tsconfig.json:

{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true
  }
}

The strict flag enables all strict type-checking options, but I like to explicitly set the others as well for clarity. This configuration has caught countless potential issues in my code before they became problems.

Check out: Iterate Over Objects in TypeScript

2. Leverage Type Inference

TypeScript’s type inference is powerful, and I’ve learned to trust it rather than over-specifying types everywhere.

// Instead of this:
const userAge: number = 30;
console.log("User age is:", userAge);

// Do this:
const userAge = 30;
console.log("User age is (inferred):", userAge);

Output:

TypeScript Best Practices

TypeScript already knows userAge is a number, so there’s no need to specify it. I save explicit type annotations for when they’re actually needed – like function parameters and return types.

3. Use Function Type Signatures

When working with functions, I always define proper type signatures for parameters and return values. This practice has made my code much more predictable and self-documenting.

// Instead of this:
function calculateTax(income) {
  return income * 0.2;
}
console.log("Tax:", calculateTax(50000)); // No type safety

// Do this:
function calculateTax(income: number): number {
  const tax = income * 0.2;
  console.log(`Calculating tax for income ${income}:`, tax); // Logs calculation
  return tax;
}
calculateTax(50000);

Output:

TypeScript type Safety best practices

For complex functions, this becomes even more valuable. It’s like having built-in documentation that’s always up-to-date.

Check out: Convert TypeScript Objects to JSON

4. Embrace Interface over Type When Appropriate

Both interface and type are useful, but I’ve found interfaces are often better for object shapes that might be extended later.

interface Employee {
  id: number;
  name: string;
  department: string;
}

interface Manager extends Employee {
  directReports: Employee[];
}

const manager: Manager = {
  id: 1,
  name: 'Sarah',
  department: 'Engineering',
  directReports: [
    { id: 2, name: 'John', department: 'Engineering' },
    { id: 3, name: 'Anna', department: 'Engineering' },
  ],
};

console.log("Manager Info:", manager);

Output:

Use interface over type in TypeaScript

The ability to extend interfaces has proven invaluable as my applications grow and evolve.

Check out: TypeScript keyof Operator

5. Use Discriminated Unions for State Management

When dealing with different states in an application, discriminated unions have been my go-to pattern:

interface User {
  id: number;
  name: string;
}

interface LoadingState {
  status: 'loading';
}

interface SuccessState {
  status: 'success';
  data: User[];
}

interface ErrorState {
  status: 'error';
  error: string;
}

type UserState = LoadingState | SuccessState | ErrorState;

function showLoadingSpinner() {
  console.log("Loading users...");
}
function renderUsers(users: User[]) {
  console.log("Rendering users:", users);
}
function showError(error: string) {
  console.error("Error fetching users:", error);
}

function handleUserState(state: UserState) {
  switch (state.status) {
    case 'loading':
      return showLoadingSpinner();
    case 'success':
      return renderUsers(state.data);
    case 'error':
      return showError(state.error);
  }
}

handleUserState({ status: 'loading' });
handleUserState({ status: 'success', data: [{ id: 1, name: 'Jane' }] });
handleUserState({ status: 'error', error: 'Failed to load data' });

Output:

Discriminated Unions best practice in TypeScript

This pattern has made my state logic much more maintainable and has eliminated entire categories of bugs.

Check out: Ternary Operator in TypeScript

6. Create Utility Types for Common Patterns

I’ve found that creating utility types for common patterns saves time and improves consistency:

interface User {
  id: number;
  name: string;
  email: string;
  password: string;
  ssn: string;
}

type UserBasicInfo = Pick<User, 'id' | 'name' | 'email'>;
type UserWithoutSensitiveData = Omit<User, 'password' | 'ssn'>;

const userBasic: UserBasicInfo = { id: 101, name: 'Alex', email: 'alex@example.com' };
const userSafe: UserWithoutSensitiveData = { id: 101, name: 'Alex', email: 'alex@example.com' };

console.log("Basic Info:", userBasic);
console.log("User without sensitive data:", userSafe);

Output:

Utility Type Best practice in TypeScript

TypeScript already includes many built-in utility types, but I often create custom ones specific to my application’s domain.

Check out: Use Comments in TypeScript

7. Use Enums for Related Constants

For related constants, I use enums instead of separate variables:

// Instead of:
const STATUS_PENDING = 'pending';
const STATUS_APPROVED = 'approved';
const STATUS_REJECTED = 'rejected';

// Do this:
enum ApprovalStatus {
  Pending = 'pending',
  Approved = 'approved',
  Rejected = 'rejected'
}

function processApplication(status: ApprovalStatus) {
  // TypeScript ensures we're using a valid status
}

This approach provides better type safety and makes the relationship between constants explicit.

8. Avoid any – Use unknown Instead

The any type defeats the purpose of TypeScript, so I avoid it whenever possible. When I need flexibility, I use unknown instead:

// Instead of:
function processData(data: any) {
  data.process(); // No type checking - dangerous!
}

// Do this:
function processData(data: unknown) {
  if (typeof data === 'object' && data !== null && 'process' in data) {
    data.process(); // Safe after checking
  }
}

Using unknown forces me to verify the type before performing operations, which has prevented many runtime errors.

9. Implement CI Checks for TypeScript Errors

One practice that’s saved my team countless hours is implementing CI checks that fail if there are any TypeScript errors:

# In GitHub Actions workflow
steps:
  - uses: actions/checkout@v2
  - uses: actions/setup-node@v2
    with:
      node-version: '16'
  - run: npm install
  - run: npm run type-check # Script that runs "tsc --noEmit"

This ensures no one can merge code with type errors, maintaining code quality across the team.

Check out: Use of Any Type in TypeScript

10. Generate Types from API Schemas

For APIs, I generate TypeScript types directly from schemas or API definitions:

interface ApiResponse {
  users: {
    id: number;
    name: string;
    email: string;
    role: 'admin' | 'user' | 'guest';
  }[];
  pagination: {
    total: number;
    page: number;
    limit: number;
  };
}

const mockApiResponse: ApiResponse = {
  users: [
    { id: 1, name: 'Sam', email: 'sam@example.com', role: 'admin' },
    { id: 2, name: 'Rita', email: 'rita@example.com', role: 'user' },
  ],
  pagination: {
    total: 50,
    page: 1,
    limit: 10,
  },
};

console.log("Fetched users:", mockApiResponse.users);
console.log("Pagination info:", mockApiResponse.pagination);

Output:

Generate Types from API Schemas in TypeScript

Tools like OpenAPI generators or GraphQL code generators have made this process almost automatic, ensuring my types always match the actual API contract.

These practices have dramatically improved the quality of my TypeScript code over the years. They’ve helped me catch errors earlier, write more maintainable code, and onboard new team members more quickly.

If you’re working with TypeScript, I encourage you to adopt these practices in your projects. They may seem like small changes, but their cumulative effect on your codebase will be significant. The initial investment in proper typing pays dividends in reduced bugs and improved developer experience.

51 Python Programs

51 PYTHON PROGRAMS PDF FREE

Download a FREE PDF (112 Pages) Containing 51 Useful Python Programs.

pyython developer roadmap

Aspiring to be a Python developer?

Download a FREE PDF on how to become a Python developer.

Let’s be friends

Be the first to know about sales and special discounts.