Menu
×
   ❮   
HTML CSS JAVASCRIPT SQL PYTHON JAVA PHP HOW TO W3.CSS C C++ C# BOOTSTRAP REACT MYSQL JQUERY EXCEL XML DJANGO NUMPY PANDAS NODEJS DSA TYPESCRIPT ANGULAR GIT POSTGRESQL MONGODB ASP AI R GO KOTLIN SASS VUE GEN AI SCIPY CYBERSECURITY DATA SCIENCE INTRO TO PROGRAMMING BASH RUST

TypeScript Best Practices


This guide covers essential TypeScript best practices to help you write clean, maintainable, and type-safe code. Following these practices will improve code quality and developer experience.


Project Configuration

Enable Strict Mode

Always enable strict mode in your tsconfig.json for maximum type safety:

// tsconfig.json
{
  "compilerOptions": {
    /* Enable all strict type-checking options */
    "strict": true,
    /* Additional recommended settings */
    "target": "ES2020",
    "module": "commonjs",
    "moduleResolution": "node",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

Enable Strict Checks

Consider enabling these additional strict checks for better code quality:

{
  "compilerOptions": {
    /* Additional strict checks */
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

Type System Best Practices

Use Type Inference Where Possible

Let TypeScript infer types when the type is obvious from the assignment:

// Bad: Redundant type annotation
const name: string = 'John';

// Good: Let TypeScript infer the type
const name = 'John';

// Bad: Redundant return type
function add(a: number, b: number): number {
  return a + b;
}

// Good: Let TypeScript infer return type
function add(a: number, b: number) {
  return a + b;
}
Try it Yourself »

Precise Type Annotations

Be explicit with types for public APIs and function parameters:

// Bad: No type information
function processUser(user) {
  return user.name.toUpperCase();
}

// Good: Explicit parameter and return types
interface User {
  id: number;
  name: string;
  email?: string; // Optional property
}

function processUser(user: User): string {
  return user.name.toUpperCase();
}

Interfaces vs. Type Aliases

Know when to use interface vs type:

// Use interface for object shapes that can be extended/implemented
interface User {
  id: number;
  name: string;
}

// Extending an interface
interface AdminUser extends User {
  permissions: string[];
}

// Use type for unions, tuples, or mapped types
type UserRole = 'admin' | 'editor' | 'viewer';

// Union types
type UserId = number | string;

// Mapped types
type ReadonlyUser = Readonly<User>;

// Tuple types
type Point = [number, number];
Try it Yourself »

Avoid any Type

Prefer more specific types over any:

// Bad: Loses type safety
function logValue(value: any) {
  console.log(value.toUpperCase()); // No error until runtime
}

// Better: Use generic type parameter
function logValue<T>(value: T) {
  console.log(String(value)); // Safer, but still not ideal
}

// Best: Be specific about expected types
function logString(value: string) {
  console.log(value.toUpperCase()); // Type-safe
}

// When you need to accept any value but still be type-safe
function logUnknown(value: unknown) {
  if (typeof value === 'string') {
    console.log(value.toUpperCase());
  } else {
    console.log(String(value));
  }
}

Code Organization

Module Organization

Organize code into logical modules with clear responsibilities:

// user/user.model.ts
export interface User {
  id: string;
  name: string;
  email: string;
}

// user/user.service.ts
import { User } from './user.model';

export class UserService {
  private users: User[] = [];

  addUser(user: User) {
    this.users.push(user);
  }

  getUser(id: string): User | undefined {
    return this.users.find(user => user.id === id);
  }
}

// user/index.ts (barrel file)
export * from './user.model';
export * from './user.service';

File Naming Conventions

Follow consistent file naming patterns:

// Good
user.service.ts // Service classes
user.model.ts // Type definitions
user.controller.ts // Controllers
user.component.ts // Components
user.utils.ts // Utility functions
user.test.ts // Test files

// Bad
UserService.ts // Avoid PascalCase for file names
user_service.ts // Avoid snake_case
userService.ts // Avoid camelCase for file names

Best Practices

  • Document your types and interfaces.
  • Prefer composition over inheritance for types.
  • Keep tsconfig.json strict and up-to-date.
  • Refactor code to use more specific types as the codebase evolves.

Functions and Methods

Function Parameters and Return Types

Write clear and type-safe functions with proper parameter and return types:

// Bad: No type information
function process(user, notify) {
  notify(user.name);
}

// Good: Explicit parameter and return types
function processUser(
  user: User,
  notify: (message: string) => void
): void {
  notify(`Processing user: ${user.name}`);
}

// Use default parameters instead of conditionals
function createUser(
  name: string,
  role: UserRole = 'viewer',
  isActive: boolean = true
): User {
  return { name, role, isActive };
}

// Use rest parameters for variable arguments
function sum(...numbers: number[]): number {
  return numbers.reduce((total, num) => total + num, 0);
}
Try it Yourself »

Avoid Function Overuse

Be mindful of function complexity and responsibilities:

// Bad: Too many responsibilities
function processUserData(userData: any) {
  // Validation
  if (!userData || !userData.name) throw new Error('Invalid user data');

  // Data transformation
  const processedData = {
    ...userData,
    name: userData.name.trim(),
    createdAt: new Date()
  };

  // Side effect
  saveToDatabase(processedData);

  // Notification
  sendNotification(processedData.email, 'Profile updated');

  return processedData;
}

// Better: Split into smaller, focused functions
function validateUserData(data: unknown): UserData {
  if (!data || typeof data !== 'object') {
    throw new Error('Invalid user data');
  }
  return data as UserData;
}

function processUserData(userData: UserData): ProcessedUserData {
  return {
    ...userData,
    name: userData.name.trim(),
    createdAt: new Date()
  };
}

Async/Await Patterns

Proper Async/Await Usage

Handle asynchronous operations effectively with proper error handling:

// Bad: Not handling errors
async function fetchData() {
  const response = await fetch('/api/data');
  return response.json();
}

// Good: Proper error handling
async function fetchData<T>(url: string): Promise<T> {
  try {
    const response = await fetch(url);
    if (!response.ok) {
      throw new Error(`HTTP error! status: ${response.status}`);
    }
    return await response.json() as T;
  } catch (error) {
    console.error('Failed to fetch data:', error);
    throw error; // Re-throw to allow caller to handle
  }
}

// Better: Use Promise.all for parallel operations
async function fetchMultipleData<T>(urls: string[]): Promise<T[]> {
  try {
    const promises = urls.map(url => fetchData<T>(url));
    return await Promise.all(promises);
  } catch (error) {
    console.error('One or more requests failed:', error);
    throw error;
  }
}

// Example usage
interface User {
  id: string;
  name: string;
  email: string;
}

// Fetch user data with proper typing
async function getUserData(userId: string): Promise<User> {
  return fetchData<User>(`/api/users/${userId}`);
}

Avoid Nested Async/Await

Flatten your async/await code to avoid callback hell:

// Bad: Nested async/await (callback hell)
async function processUser(userId: string) {
  const user = await getUser(userId);
  if (user) {
    const orders = await getOrders(user.id);
    if (orders.length > 0) {
      const latestOrder = orders[0];
      const items = await getOrderItems(latestOrder.id);
      return { user, latestOrder, items };
    }
  }
  return null;
}

// Better: Flatten the async/await chain
async function processUser(userId: string) {
  const user = await getUser(userId);
  if (!user) return null;

  const orders = await getOrders(user.id);
  if (orders.length === 0) return { user, latestOrder: null, items: [] };

  const latestOrder = orders[0];
  const items = await getOrderItems(latestOrder.id);

  return { user, latestOrder, items };
}

// Best: Use Promise.all for independent async operations
async function processUser(userId: string) {
  const [user, orders] = await Promise.all([
    getUser(userId),
    getOrders(userId)
  ]);

  if (!user) return null;
  if (orders.length === 0) return { user, latestOrder: null, items: [] };

  const latestOrder = orders[0];
  const items = await getOrderItems(latestOrder.id);

  return { user, latestOrder, items };
}

Testing and Quality

Writing Testable Code

Design your code with testability in mind by using dependency injection and pure functions:

// Bad: Hard to test due to direct dependencies
class PaymentProcessor {
  async processPayment(amount: number) {
    const paymentGateway = new PaymentGateway();
    return paymentGateway.charge(amount);
  }
}

// Better: Use dependency injection
interface PaymentGateway {
  charge(amount: number): Promise<boolean>;
}

class PaymentProcessor {
  constructor(private paymentGateway: PaymentGateway) {}

  async processPayment(amount: number): Promise<boolean> {
    if (amount <= 0) {
      throw new Error('Amount must be greater than zero');
    }
    return this.paymentGateway.charge(amount);
  }
}

// Test example with Jest
describe('PaymentProcessor', () => {
  let processor: PaymentProcessor;
  let mockGateway: jest.Mocked<PaymentGateway>;

  beforeEach(() => {
    mockGateway = {
      charge: jest.fn()
    };
    processor = new PaymentProcessor(mockGateway);
  });

  it('should process a valid payment', async () => {
    mockGateway.charge.mockResolvedValue(true);
    const result = await processor.processPayment(100);
    expect(result).toBe(true);
    expect(mockGateway.charge).toHaveBeenCalledWith(100);
  });

  it('should throw for invalid amount', async () => {
    await expect(processor.processPayment(-50))
      .rejects
      toThrow('Amount must be greater than zero');
  });
});

Type Testing

Test your types to ensure they work as expected using type assertions and utilities:

// Using @ts-expect-error to test for type errors
// @ts-expect-error - Should not allow negative values
const invalidUser: User = { id: -1, name: 'Test' };

// Using type assertions in tests
function assertIsString(value: unknown): asserts value is string {
  if (typeof value !== 'string') {
    throw new Error('Not a string');
  }
}

// Using utility types for testing
type IsString<T> = T extends string ? true : false;
type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false

// Using tsd for type testing (install with: npm install --save-dev tsd)
/*
import { expectType } from 'tsd';

const user = { id: 1, name: 'John' };
expectType<{ id: number; name: string }>(user);
expectType<string>(user.name);
*/

Performance Considerations

Type-Only Imports and Exports

Use type-only imports and exports to reduce bundle size and improve tree-shaking:

// Bad: Imports both type and value
import { User, fetchUser } from './api';

// Good: Separate type and value imports
import type { User } from './api';
import { fetchUser } from './api';

// Even better: Use type-only imports when possible
import type { User, UserSettings } from './types';

// Type-only export
export type { User };

// Runtime export
export { fetchUser };

// In tsconfig.json, enable "isolatedModules": true
// to ensure type-only imports are properly handled

Avoid Excessive Type Complexity

Be mindful of complex types that can impact compilation time:

// Bad: Deeply nested mapped types can be slow
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

// Better: Use built-in utility types when possible
type User = {
  id: string;
  profile: {
    name: string;
    email: string;
  };
  preferences?: {
    notifications: boolean;
  };
};

// Instead of DeepPartial<User>, use Partial with type assertions
const updateUser = (updates: Partial<User>) => {
  // Implementation
};

// For complex types, consider using interfaces
interface UserProfile {
  name: string;
  email: string;
}

interface UserPreferences {
  notifications: boolean;
}

interface User {
  id: string;
  profile: UserProfile;
  preferences?: UserPreferences;
}

Use const Assertions for Literal Types

Improve type inference and performance with const assertions:

// Without const assertion (wider type)
const colors = ['red', 'green', 'blue'];
// Type: string[]

// With const assertion (narrower, more precise type)
const colors = ['red', 'green', 'blue'] as const;
// Type: readonly ["red", "green", "blue"]

// Extract union type from const array
type Color = typeof colors[number]; // "red" | "green" | "blue"

// Objects with const assertions
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  features: ['auth', 'notifications'],
} as const;

// Type is:
// {
// readonly apiUrl: "https://api.example.com";
// readonly timeout: 5000;
// readonly features: readonly ["auth", "notifications"];
// }
Try it Yourself »

Common Mistakes to Avoid

Overusing the any Type

Avoid using `any` as it defeats TypeScript's type checking:

// Bad: Loses all type safety
function process(data: any) {
  return data.map(item => item.name);
}

// Better: Use generics for type safety
function process<T extends { name: string }>(items: T[]) {
  return items.map(item => item.name);
}

// Best: Use specific types when possible
interface User {
  name: string;
  age: number;
}

function processUsers(users: User[]) {
  return users.map(user => user.name);
}

Not Using Strict Mode

Always enable strict mode in your `tsconfig.json`:

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    /* Additional strictness flags */
    "noImplicitAny": true,
    "strictNullChecks": true,
    "strictFunctionTypes": true,
    "strictBindCallApply": true,
    "strictPropertyInitialization": true,
    "noImplicitThis": true,
    "alwaysStrict": true
  }
}

Ignoring Type Inference

Let TypeScript infer types when possible:

// Redundant type annotation
const name: string = 'John';

// Let TypeScript infer the type
const name = 'John'; // TypeScript knows it's a string

// Redundant return type
function add(a: number, b: number): number {
  return a + b;
}

// Let TypeScript infer the return type
function add(a: number, b: number) {
  return a + b; // TypeScript infers number
}

Not Using Type Guards

Use type guards to narrow types safely:

// Without type guard
function process(input: string | number) {
  return input.toUpperCase(); // Error: toUpperCase doesn't exist on number
}

// With type guard
function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function process(input: string | number) {
  if (isString(input)) {
    return input.toUpperCase(); // TypeScript knows input is string here
  } else {
    return input.toFixed(2); // TypeScript knows input is number here
  }
}

// Built-in type guards
if (typeof value === 'string') { /* value is string */ }
if (value instanceof Date) { /* value is Date */ }
if ('id' in user) { /* user has id property */ }
Try it Yourself »

Not Handling null and undefined

Always handle potential `null` or `undefined` values:

// Bad: Potential runtime error
function getLength(str: string | null) {
  return str.length; // Error: Object is possibly 'null'
}

// Good: Null check
function getLength(str: string | null) {
  if (str === null) return 0;
  return str.length;
}

// Better: Use optional chaining and nullish coalescing
function getLength(str: string | null) {
  return str?.length ?? 0;
}

// For arrays
const names: string[] | undefined = [];
const count = names?.length ?? 0; // Safely handle undefined

// For object properties
interface User {
  profile?: {
    name?: string;
  };
}

const user: User = {};
const name = user.profile?.name ?? 'Anonymous';



×

Contact Sales

If you want to use W3Schools services as an educational institution, team or enterprise, send us an e-mail:
[email protected]

Report Error

If you want to report an error, or if you want to make a suggestion, send us an e-mail:
[email protected]

W3Schools is optimized for learning and training. Examples might be simplified to improve reading and learning. Tutorials, references, and examples are constantly reviewed to avoid errors, but we cannot warrant full correctness of all content. While using W3Schools, you agree to have read and accepted our terms of use, cookie and privacy policy.

Copyright 1999-2025 by Refsnes Data. All Rights Reserved. W3Schools is Powered by W3.CSS.