Writing clean, clear, and efficient TypeScript code is crucial for maintaining a scalable and maintainable codebase. This article will cover 20 practical tips and code examples to help you improve your TypeScript development skills and produce high-quality code.

1. Use Explicit Types Instead of "any"

Avoid using the any type whenever possible, as it undermines TypeScript's benefits. Instead, explicitly define the types for variables, functions, and parameters.

👍do this:

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

👎instead of this:

function add(a: any, b: any): any {
  return a + b;
}

2. Enable "strict" Mode in tsconfig.json

Enabling the "strict" mode ensures TypeScript performs extensive type checking, catching potential errors early in the development process.

{
 "compilerOptions": {
 "strict": true
 }
}

3. Use Readonly Arrays

Make use of readonly to prevent accidental modifications to your objects and arrays.

👍do this:

const person: Readonly<{ name: string; age: number }> = { name: 'Alice', age: 30 };
person.age = 31; // Error: Cannot assign to 'age' because it is a read-only property

const numbers: ReadonlyArray<number> = [1, 2, 3];
numbers.push(4); // Error: Property 'push' does not exist on type 'readonly number[]'

👎instead of this:

const person = { name: 'Alice', age: 30 };
person.age = 31; // Allowed

const numbers = [1, 2, 3];
numbers.push(4); // Allowed

4. Use Destructuring to Extract Properties

Destructuring can make your code more concise and easier to read.

👍do this:

function printPersonDetails({ name, age }: { name: string; age: number }) {
  console.log(`Name: ${name}, Age: ${age}`);
}

👎instead of this:

function printPersonDetails(person: { name: string; age: number }) {
  console.log(`Name: ${person.name}, Age: ${person.age}`);
}

5. Array Generics over Type Casting

Use array generics to specify the type of elements in an array instead of type casting.

👍do this:

const numbers: Array<number> = [1, 2, 3];
const firstNumber: number = numbers[0];

👎instead of this:

const numbers: any[] = [1, 2, 3];
const firstNumber: number = numbers[0] as number;

6. Utilize Enums for Constants

Use enums for a set of related constants to improve code readability and maintainability.

👍do this:

enum Fruit {
  APPLE = 'apple',
  BANANA = 'banana',
  ORANGE = 'orange',
}

👎instead of this:

const FRUIT_APPLE = 'apple';
const FRUIT_BANANA = 'banana';
const FRUIT_ORANGE = 'orange';

7. Prefer Interface over Type Alias for Object Shapes

Use interfaces when defining the shape of an object to leverage their extendability.

👍do this:

interface Person {
  name: string;
  age: number;
}

👎instead of this:

type Person = {
  name: string;
  age: number;
};

8. Use Optional Properties for Configurable Object

Use optional properties in interfaces to allow flexibility when configuring objects.

👍do this:

interface Person {
  name: string;
  age?: number;
}

👎instead of this:

interface Person {
  name: string;
  age?: number;
}

9. Use TypeScript's Utility Types

Leverage TypeScript's built-in utility types, such as Partial, Pick, and Omit, to avoid unnecessary duplication and simplify your code.

interface Person {
  name: string;
  age: number;
  address: string;
}

type PartialPerson = Partial<Person>; // Makes all properties optional
type PersonName = Pick<Person, 'name'>; // Extracts a subset of properties
type PersonWithoutAge = Omit<Person, 'age'>; // Removes a property

10. Use Union Types for Multiple Possible Types

Use union types to specify that a variable can hold values of multiple types.

👍do this:

function formatInput(input: string | number) {
  return `Input: ${input}`;
}

👎instead of this:

function formatInput(input: string | number) {
  return `Input: ${input}`;
}

11. Utilize Intersection Types for Combining Types

Use intersection types to combine multiple types into a single type.

👍do this:

interface Shape {
  color: string;
}

interface Circle {
  radius: number;
}

interface Rectangle {
  width: number;
  height: number;
}

type RedCircle = Shape & Circle;
type RedRectangle = Shape & Rectangle;

const redCircle: RedCircle = { color: 'red', radius: 5 };
const redRectangle: RedRectangle = { color: 'red', width: 10, height: 20 };

👎instead of this:

interface Employee {
  name: string;
  age: number;
}

interface Manager {
  teamSize: number;
}

type EmployeeManager = Employee & Manager;

const employee: EmployeeManager = { name: 'John Doe', age: 30, teamSize: 5 };

12. Use Type Guards for Type Assertions

Use type guards to narrow down the type of a variable within a conditional block.

👍do this:

function formatValue(value: string | number): string {
  if (typeof value === 'number') {
    return value.toFixed(2);
  } else if (typeof value === 'string') {
    return value.toUpperCase();
  } else {
    throw new Error('Invalid value');
  }
}

👎instead of this:

function processValue(value: string | number): string {
  if (typeof value === 'number') {
    return value.toFixed(2);
  } else {
    return value.toUpperCase();
  }
}

13. Prefer Functional Programming Techniques

Utilize functional programming techniques, such as immutability and pure functions, to improve code clarity and reduce side effects.

👍do this:

const sum = Array.from({ length: 10 }, (_, i) => i + 1).reduce((acc, val) => acc + val, 0);

👎instead of this:

let sum = 0;
for (let i = 1; i <= 10; i++) {
  sum += i;
}

14. Use Nullish Coalescing Operator (??)

The nullish coalescing operator (??) provides a concise way to handle null or undefined values.

👍do this:

const defaultValue = value ?? 'Default';

👎instead of this:

const defaultValue = value !== null && value !== undefined ? value : 'Default';

15. Use Optional Chaining (?.)

Optional chaining (?.) simplifies accessing properties of an object that may be undefined or null.

👍do this:

const username = user?.profile?.name;

👎instead of this:

const username = user && user.profile && user.profile.name;

16. Leverage Type Inference

Leverage TypeScript's type inference capabilities to avoid redundant type annotations.

👍do this:

const name = 'Alice';

👎instead of this:

const name: string = 'Alice';

17. Avoid Deep Nesting

Leverage TypeScript's type inference capabilities to avoid redundant type annotations.

👍do this:

function process() {
  // Code
}

if (condition1 && condition2 && condition3) {
  process();
}

👎instead of this:

if (condition1) {
  if (condition2) {
    if (condition3) {
      // Code
    }
  }
}

18. Follow Consistent Naming Conventions

Adhere to consistent naming conventions for variables, functions, and classes to improve code readability. Use descriptive names that convey the purpose of the entity.

19. Modularize Your Code

Break down your code into smaller modules, each responsible for a specific functionality. This promotes reusability and maintainability.

20. Write Clear and Concise Comments

Add comments to explain complex algorithms, important decisions, or edge cases. Avoid excessive comments that merely restate the code.

Conclusion: Writing clear and efficient TypeScript code requires practice, attention to detail, and adherence to best practices. By following the 20 tips outlined in this article, you'll be able to produce high-quality code that is easier to understand, maintain, and scale. Happy coding!