DEV Community

Cover image for Best Practices for Writing Clean TypeScript Code 🚀
Ali Samir
Ali Samir

Posted on

Best Practices for Writing Clean TypeScript Code 🚀

TypeScript has become a popular choice for JavaScript developers who want a more structured approach to their code.

Its strong typing system and enhanced features make catching bugs early and managing large projects easier.

However, as with any language, writing clean and maintainable TypeScript requires following some best practices.

Here are 10 tips to help you keep your TypeScript codebase clean, readable, and scalable.


1- Use Strict Typing

TypeScript offers a --strict flag that enables several strict checks, like noImplicitAny and strictNullChecks.

Enabling strict typing helps catch potential bugs and forces you to declare types explicitly. Here’s how to enable strict mode in tsconfig.json:

{
  "compilerOptions": {
    "strict": true
  }
}
Enter fullscreen mode Exit fullscreen mode

Using strict mode prevents TypeScript from inferring any type where it can't determine the specific type, reducing ambiguity and enhancing code quality.


2- Avoid any and Use Specific Types

Although any may seem convenient, it undermines TypeScript's type-checking.

Instead, try to use specific types (string, number, boolean, Date, etc.), or create custom types/interfaces to define object structures.

This keeps your code more readable and maintains strong typing.

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

function getUserData(user: User) {
  console.log(user.name);
}
Enter fullscreen mode Exit fullscreen mode

3- Leverage Type Inference

TypeScript can infer types in many situations, such as when initializing variables.

Avoid redundant type annotations where TypeScript can infer the type, as it can make your code cleaner.

// Explicit typing (less clean)
const age: number = 25;

// Inferred typing (cleaner)
const age = 25;
Enter fullscreen mode Exit fullscreen mode

Rely on inference when possible, but use explicit types when the code is ambiguous or for complex objects.


4- Use Union and Intersection Types Wisely

Union (|) and intersection (&) types in TypeScript allow you to create flexible types that can combine multiple types or properties.

Using them correctly helps make code modular and more understandable.

type Admin = {
  id: number;
  role: string;
};

type User = {
  name: string;
  email: string;
};

type SuperUser = Admin & User; // Intersection Type
Enter fullscreen mode Exit fullscreen mode

Use union types to handle cases where a variable can be one of several types and intersection types to combine types when necessary.


5- Implement Interfaces Over Type Aliases for Objects

While both interfaces and type aliases allow you to define the shapes of objects, interfaces are more flexible and scalable, especially when extending or merging is required.

Use interfaces for defining object structures and type aliases for other scenarios like unions.

interface Vehicle {
  make: string;
  model: string;
  year: number;
}

const car: Vehicle = {
  make: "Toyota",
  model: "Corolla",
  year: 2020
};
Enter fullscreen mode Exit fullscreen mode

Interfaces can be extended, making them more suited for object definitions.


6- Keep Your Code DRY (Don’t Repeat Yourself)

Avoid redundancy in your TypeScript code by using generics, utility types, and helper functions to create reusable components.

Generics are especially useful in functions and classes when working with various types.

function wrapInArray<T>(value: T): T[] {
  return [value];
}
Enter fullscreen mode Exit fullscreen mode

Using generics, you can avoid repeating similar code, making it more reusable and flexible.


7- Handle Null and Undefined Properly

TypeScript’s strictNullChecks help catch cases where null or undefined values might be used.

Always check for null values when accessing properties that might be null or undefined.

function printUser(user?: User) {
  if (user) {
    console.log(user.name);
  }
}
Enter fullscreen mode Exit fullscreen mode

Using optional chaining (?.) and the nullish coalescing operator (??) can also make your code cleaner and more robust.


8- Use readonly for Immutable Data

Use readonly on properties that shouldn’t change once initialized.

This is especially useful in function parameters and class properties to prevent accidental mutation.

interface Config {
  readonly apiKey: string;
  readonly timeout: number;
}

const config: Config = {
  apiKey: "12345",
  timeout: 3000,
};

// config.apiKey = "67890"; // Error
Enter fullscreen mode Exit fullscreen mode

Making properties readonly can prevent unintended changes, enhancing the code's stability.


9- Prefer Enum and Constant Types for Fixed Values

For values that don’t change, such as status codes, error codes, or fixed options, use enums or constant unions to provide a predefined set of possible values.

This reduces the risk of errors from invalid values.

enum Status {
  Active = "ACTIVE",
  Inactive = "INACTIVE",
  Pending = "PENDING",
}

function updateStatus(status: Status) {
  console.log(status);
}
Enter fullscreen mode Exit fullscreen mode

Enums make your code more readable and prevent mistakes from string literals or magic numbers.


10- Document Your Code

Adding JSDoc-style comments to your TypeScript code helps others understand your intent and usage.

TypeScript can even leverage JSDoc comments to display type hints and documentation in IDEs.

/**
 * Fetches user data from the server.
 * @param userId - The ID of the user to fetch.
 * @returns A promise that resolves to user data.
 */
function fetchUserData(userId: number): Promise<User> {
  return api.get(`/users/${userId}`);
}
Enter fullscreen mode Exit fullscreen mode

Good documentation can make your code easier to work with, especially for large codebases or teams.



Conclusion âś…

By adhering to these best practices, you’ll discover that your TypeScript code becomes cleaner, more robust, and easier to maintain.

Clean code is not only easier to read and debug, but it also scales better, benefiting both solo developers and large teams.

Leveraging TypeScript’s powerful type system alongside these principles can significantly enhance the quality and longevity of your projects.


Happy Coding!

Top comments (2)

Collapse
 
alisamir profile image
Ali Samir

Join our channel for top-notch programming hacks, epic discussions, and brilliant career moves. 🚀
t.me/the_developer_guide

Collapse
 
michaelandish profile image
Info Comment hidden by post author - thread only accessible via permalink
Michael

Actually, I don't agree with that last part; we defined TYPE hint, return type, and the function name already explained what it does, so why do we need those useless documents on top of it?!

function fetchUserData(userId: number): Promise<User> {
return api.get(
/users/${userId});
}

Some comments have been hidden by the post's author - find out more