DEV Community

Yug Jadvani
Yug Jadvani

Posted on

How to Write Better TypeScript Code: Best Practices for Clean, Effective, and Scalable Code

Introduction

As TypeScript has grown in popularity, developers have embraced it for its type safety, scalability, and powerful tooling in modern web applications. Whether you’re a beginner or an advanced developer, refining your TypeScript skills can make a significant difference in the quality, maintainability, and readability of your codebase. This guide will walk you through practical tips and best practices to help you write better TypeScript code.


Why Focus on Writing Better TypeScript Code?

TypeScript brings robust typing and tooling, but the way you write your code matters when it comes to delivering value, reducing errors, and maintaining clean, readable code. Knowing how to utilize TypeScript’s features effectively ensures:

  • Reduced Bugs: Strong typing can help prevent many runtime errors by catching issues during compile time.
  • Improved Code Quality: Clean TypeScript code is easier to understand, test, and maintain.
  • Enhanced Collaboration: Clear types and interfaces make your codebase easier for others to pick up and work on.

1. Leverage Strict Typing Options

TypeScript’s compiler options allow you to enforce stricter type-checking rules. Setting "strict": true in your tsconfig.json is a great starting point, but consider enabling additional options like:

  • "noImplicitAny": Avoids using the any type unintentionally.
  • "strictNullChecks": Ensures variables cannot be null or undefined unless explicitly allowed.
  • "strictFunctionTypes": Enforces correct function type inference, preventing subtle bugs.

Stricter typing often reveals hidden bugs and makes your codebase more reliable.


2. Use Types and Interfaces Wisely

Both type and interface allow you to define the shape of objects, but each has its strengths:

  • Interface: Use interfaces when defining objects, especially when you expect objects to have a consistent shape that could benefit from inheritance or future extension.
interface User {
  id: number;
  name: string;
  email: string;
}
Enter fullscreen mode Exit fullscreen mode

Type: Use types for unions or creating more complex data structures where you don’t need extension.

type Status = "active" | "inactive" | "pending";
Enter fullscreen mode Exit fullscreen mode

Understanding when to use each will lead to more maintainable, predictable code.


3. Prefer unknown Over any

The any type is often misused, as it allows any kind of data, defeating TypeScript's purpose of type safety. Instead, opt for unknown when the type is uncertain. Unlike any, unknown requires type-checking before you can perform operations on it, enforcing safety.

function processInput(input: unknown) {
  if (typeof input === "string") {
    console.log(input.toUpperCase());
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Use Readonly and Immutable Types for Safety

Adding readonly to properties can prevent accidental mutation, which is valuable in many scenarios, particularly when dealing with data structures that shouldn't change once initialized.

interface Product {
  readonly id: number;
  readonly name: string;
  price: number;
}
Enter fullscreen mode Exit fullscreen mode

Using readonly for properties that shouldn't be altered reduces bugs and clarifies the immutability of certain data in your code.


5. Define Utility Types for Reusability

TypeScript offers several utility types (Partial, Pick, Omit, Readonly, etc.) that make your code more concise and help avoid repetitive definitions. For instance, if you want a version of User with all optional properties, use Partial<User>.

type OptionalUser = Partial<User>;
Enter fullscreen mode Exit fullscreen mode

These utility types simplify handling variations in types, making your code more versatile.


6. Define Return Types Explicitly

When defining functions, always specify the return type. This not only makes the code more predictable but also helps TypeScript catch errors if the function behavior changes later.

function getUser(id: number): User | null {
  // logic to fetch user
}
Enter fullscreen mode Exit fullscreen mode

Explicit return types reduce ambiguity and help ensure the function behaves as expected.


7. Handle Null and Undefined Safely

Types like null and undefined often cause runtime errors if not handled properly. TypeScript provides the optional chaining (?.) and nullish coalescing (??) operators to handle these cases gracefully.

const userName = user?.profile?.name ?? "Guest";
Enter fullscreen mode Exit fullscreen mode

These operators help you avoid common pitfalls around null values without complex, nested checks.


8. Utilize Enum for Meaningful Values

Enums in TypeScript allow you to define a set of named constants. They make your code more expressive and prevent the use of arbitrary strings or numbers.

enum UserRole {
  Admin = "ADMIN",
  User = "USER",
  Guest = "GUEST",
}

function assignRole(role: UserRole) {
  // logic here
}
Enter fullscreen mode Exit fullscreen mode

Enums are particularly useful when dealing with a fixed set of options, making the code self-documenting and reducing errors.


9. Use never for Exhaustive Checks

When working with union types, use never to ensure exhaustive checking in switch cases. This ensures that if new cases are added to the union, TypeScript will throw an error if they are not handled.

type Shape = Circle | Square | Triangle;

function getArea(shape: Shape) {
  switch (shape.type) {
    case "circle":
      return Math.PI * shape.radius ** 2;
    case "square":
      return shape.side * shape.side;
    default:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}
Enter fullscreen mode Exit fullscreen mode

This never type-checking technique reduces the risk of unhandled cases, ensuring your code is exhaustive and type-safe.


10. Keep Functions Pure and Concise

Writing pure functions — functions without side effects — helps prevent unpredictable behavior and makes testing simpler. TypeScript shines when used in functional programming as it enforces purity in function design, encouraging you to keep functions concise and predictable.

function add(a: number, b: number): number {
  return a + b;
}
Enter fullscreen mode Exit fullscreen mode

Pure functions are easier to test, debug, and understand, making your TypeScript code more robust.


Conclusion

Writing better TypeScript code means focusing on strong typing, code consistency, and best practices that make your codebase more resilient, maintainable, and scalable. As you apply these tips, you’ll find that your TypeScript code becomes cleaner, less error-prone, and more enjoyable to work with. Remember, writing better TypeScript is a continuous journey, and these practices will only grow your skills further.

Call to Action

Ready to level up your TypeScript skills? Try implementing these tips in your next project and see how they improve your development process. Let’s make TypeScript code better, one line at a time!


Enjoyed the read? If you found this article insightful or helpful, consider supporting my work by buying me a coffee. Your contribution helps fuel more content like this. Click here to treat me to a virtual coffee. Cheers!

Top comments (25)

Collapse
 
damian_cyrus profile image
Damian Cyrus

Good sum up.

This is my experience and opinion, but I don't use: 6. Define Return Types Explicitly.
I do it only when there is more than one return type. If a single type is expected, and the function is clear about it, then I leave it out. TypeScript is good with it. It makes the code more readable (for me). Simple functions stay nice and clean without too much pepper included into the code, staying closer to JavaScript.

If you do unit tests and code coverage, then these simple return types are fast covered, too. The IDE also gives hints about the return type.

That is, of course, possible with simple projects/code with less developers, or just one, and trust in their skills. For bigger teams I also suggest more strictness as a rule for safety and consistency.

The thanks for the article. 🙂

Collapse
 
yugjadvani profile image
Yug Jadvani

Your Welcome @damian_cyrus

Collapse
 
worx profile image
Worx R

This article feels AI generated again. Do NOT use enums in TypeScript, check out this actually well written article which explains why: totaltypescript.com/why-i-dont-lik...

Collapse
 
rappayne profile image
Rap Payne

I came to the comments to say exactly this. Do not use enums in TypeScript. In fact, never use TypeScript syntax when JavaScript syntax will do the trick.

Collapse
 
yugjadvani profile image
Yug Jadvani

Absolutely, that’s a solid approach! Using plain JavaScript syntax in TypeScript where possible keeps the code simple, more flexible, and easier for others to read and maintain. TypeScript’s strength really shines when it enhances, rather than complicates, JavaScript. Thanks for sharing this, it’s a great reminder to stick with simplicity where it serves best!

Collapse
 
glensc profile image
Elan Ruusamäe

Also feels like comment responses are ai generated..even the absolutely and you are right exclamations are there.

Collapse
 
brense profile image
Rense Bakker

Good list, but I disagree on number 6. One of the best things about typescript is how good it is at inferring types. This is way better than strictly typed languages, like Java, where the type system (severely) limits the implementation. You get the same amount of type safety without any of the frustration. Inferred doesn't mean its not typed. The only time I use explicit return types is when doing method overloading.

Collapse
 
yugjadvani profile image
Yug Jadvani

Great point! TypeScript’s type inference is indeed powerful, and leveraging it can make the code cleaner and often more maintainable without losing type safety. Explicit return types can sometimes feel restrictive, especially when TypeScript does such a great job of understanding context on its own. Thanks for highlighting this – it's a solid reminder to strike the right balance and only enforce explicit typing where it adds clarity or when overloads are in play. Appreciate your input! 👏

Collapse
 
kwoodgmr profile image
kwood-gmr

But... if you specify the return type - you also protect against unintended return types. I find it depends on the complexity. I don't specify for obvious - but do when it could have accidental drift.

Collapse
 
yugjadvani profile image
Yug Jadvani

Absolutely, that's a great point! Specifying return types can definitely serve as a guardrail, especially when working with more complex functions or code that could easily evolve and "drift" over time. It’s all about balancing clarity and flexibility—explicit types can be a safety net in places where the logic might change, but for simpler, more obvious functions, inference keeps things clean and readable. Thanks for adding that perspective! 👍

Collapse
 
kwoodgmr profile image
kwood-gmr

Strongly disagree with number 8. I would say don't use Enums. A much better system is to use something along the lines of:
`export const UserRole {
Admin = "ADMIN",
User = "USER",
Guest = "GUEST",
} as const

export type UserRole = (typeof UserRole) keyof UserRole`

For more thorough explanation:
blog.logrocket.com/why-typescript-...
medium.com/@alex.roh/typescript-en...

Collapse
 
yugjadvani profile image
Yug Jadvani

You make a solid case! Using as const with objects and keyof is often a much cleaner and type-safe way to define constants like user roles in TypeScript, without some of the quirks that come with enums. This approach offers better flexibility and reduces potential issues when compiling to JavaScript. Thank you for sharing those resources—I'll take a closer look and keep this technique in mind for future posts! 👏

Collapse
 
trplx_gaming profile image
Gabriel Ibe

Really nice read NGL 🤝

10. Keep Functions pure....
Yeah I don't do that😅, there are functions that can perform side effects whilst still being easy to debug based on their naming. And even in the case of modules, side effects are ok. I'm speaking from game dev experience

7. Null Coalescing and Safety
Your implementation of it was very concise and shows the minimum potential for that in large scale project development

And yes, I define the return type of my functions🤓

Collapse
 
snowman524 profile image
Snowman

Good article!

Collapse
 
yugjadvani profile image
Yug Jadvani

You're very welcome! 😊 If you have any more questions or need further insights, feel free to reach out. Happy coding! 🚀

Collapse
 
snowman524 profile image
Snowman

Thank you very much.

Collapse
 
mdrijwan profile image
Md Rijwan Razzaq Matin

Nicely compiled. Good one Yug!

Collapse
 
yugjadvani profile image
Yug Jadvani

You're very welcome! 😊 If you have any more questions or need further insights, feel free to reach out. Happy coding! 🚀

Collapse
 
glensc profile image
Elan Ruusamäe

Your 9 doesn't follow previous rule no 6. How do you solve following both?

Collapse
 
yugjadvani profile image
Yug Jadvani

Use explicit return types on complex, reusable functions where unintended return types could introduce bugs—especially in large codebases.

For functions that need exhaustive checks (like switch statements on union types), you can let TypeScript infer the return type if the function is self-contained and straightforward. If the function grows or reusability becomes important, you can then add an explicit return type.

Collapse
 
nozibul_islam_113b1d5334f profile image
Nozibul Islam

Great, thank you.

Collapse
 
yugjadvani profile image
Yug Jadvani

You're very welcome! 😊 If you have any more questions or need further insights, feel free to reach out. Happy coding! 🚀

Collapse
 
wizard798 profile image
Wizard

Just amazing tips and tricks for making code nore effective, readable and easily understandable.

Collapse
 
yugjadvani profile image
Yug Jadvani

Thanks so much! 😊 Glad you found the tips helpful. Writing code that's both effective and easy to understand is always the goal. Appreciate the positive feedback! 🙌✨