11 Best practices you should follow in Typescript

11 Best practices you should follow in Typescript

Typescript is a powerful superset of JavaScript that provides optional static typing, class-based object-oriented programming, and other features that make it a great choice for large-scale projects. However, to make the most of Typescript, it's important to follow some best practices.

Use interfaces for type checking:

Interfaces allow you to define the structure of an object and ensure that objects being passed around in your code conform to that structure. For example, if you have a function that takes an object as a parameter, you can use an interface to define the shape of the object, like so:

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

function greetUser(user: User) {
  console.log(`Hello, ${user.name}!`);
}

const myUser = { name: 'John', age: 30 };
greetUser(myUser); // This will work

const wrongUser = { name: 'John' };
greetUser(wrongUser); // This will throw an error

Make use of classes:

Typescript's class-based object-oriented programming features make it easy to create reusable, modular code. Use classes to organize your code and make it more readable and maintainable. For example:

class Person {
  constructor(public name: string, public age: number) {}

  sayHello() {
    console.log(`Hello, my name is ${this.name}.`);
  }
}

const me = new Person('John', 30);
me.sayHello(); // Outputs: "Hello, my name is John."

Avoid using "any" type:

The "any" type can be used to bypass Typescript's type checking, but it also makes it harder to understand the structure of your code and can lead to unexpected bugs. Instead, try to be specific about the types of variables and objects in your code.

const foo: any = "foo";
const bar: unknown = "bar";

foo.length; // Works, type checking is effectively turned off for this
bar.length; // Errors, bar is unknown

if (typeof bar === "string") {
  bar.length; // Works, we now know that bar is a string
}

Use access modifiers for classes

like java, Class access modifiers are included with TypeScript. The properties of these access modifiers differ. We have access modifiers that are public, protected, or private.

  • private: only accessible inside the class.

  • protected: only accessible inside the class and through subclasses.

  • public: accessible anywhere.


class Student{
  protected name: string;
  private marks: number;

  constructor(name: string, marks: number) {
    this.name = name;
    this.marks = marks
  }

  public getMarks(){
    return marks
  }
}

Here, you cannot access Marks unless you use the getMarks method

class Child extends Student {
  viewDetails() {
    console.log(this.marks); // error: property 'marks’' is private
    console.log(this.getMarks()); // success
  }
}

Use type guards:

Type guards are a way to check the type of a variable at runtime and provide different behavior based on the type. For example:

function isNumber(x: any): x is number {
  return typeof x === 'number';
}

function doSomething(x: any) {
  if (isNumber(x)) {
    console.log(`x is a number: ${x}`);
  } else {
    console.log(`x is not a number`);
  }
}

doSomething(5);  // Outputs: "x is a number: 5"
doSomething('hello');  // Outputs: "x is not a number"

Consistent with your code formatting and naming conventions:

Consistency in code formatting and naming conventions can make it easier for other developers to understand your code and contribute to the project.

Using Enums:

Enums, short for enumerations, is a TypeScript syntax for defining a set of named constants. By assigning a meaningful name to a bunch of linked values, they can be utilized to generate more understandable and manageable code.

For example, you can use an enum to specify a set of possible order status values:

enum OrderStatus {
 Pending,
 Processing,
 Shipped,
 Delivered,
 Cancelled
}
let orderStatus: OrderStatus = OrderStatus.Pending;

Enums can also have a specific set of numeric or textual values.

enum OrderStatus {
 Pending = 1,
 Processing = 2,
 Shipped = 3,
 Delivered = 4,
 Cancelled = 5
}
let orderStatus: OrderStatus = OrderStatus.Pending;

As part of the naming convention, always name an enum with the initial capital letter, and the name must be in the singular form.

Using Namespaces:

Namespaces help you structure your code and avoid name conflicts. They enable you to build a container for your code in which variables, classes, functions, and interfaces can be defined.

A namespace, for example, can be used to collect all code relevant to a specific feature:

namespace OrderModule {
 export class Order { /* … */ }
 export function cancelOrder(order: Order) { /* … */ }
 export function processOrder(order: Order) { /* … */ }
}
let order = new OrderModule.Order();
OrderModule.cancelOrder(order);

You can also use namespaces to prevent naming clashes by giving your code a unique name:

namespace MyCompany.MyModule {
 export class MyClass { /* … */ }
}
let myClass = new MyCompany.MyModule.MyClass();

Namespaces are similar to modules in that they are used to organize code and prevent naming clashes, whereas modules are used to load and execute code.

Using Utility Types:

Utility types are a TypeScript feature that provides a set of predefined types to assist you in writing better type-safe code. They enable you to conduct common type operations and modify types more easily.

For example, the Pick utility type can be used to extract a subset of properties from an object type:

type User = { name: string, age: number, email: string };
type UserInfo = Pick<User, "name" | "email">;

To remove properties from an object type, you can also use the Exclude utility type:

type User = { name: string, age: number, email: string };
type UserWithoutAge = Exclude<User, "age">;

To make all properties of a type optional, use the Partial utility type:

type User = { name: string, age: number, email: string };
type PartialUser = Partial<User>;

Using the infer keyword:

The infer keyword is a powerful TypeScript feature that allows you to determine the type of a variable within a type.

You can, for example, use the infer keyword to define a more precise type for a function that returns an array of a specified type:

type ArrayType<T> = T extends (infer U)[] ? U : never;
type MyArray = ArrayType<string[]>; // MyArray is of type string

You can also use the infer keyword to define more detailed types for functions that return objects with specific properties:

type ObjectType<T> = T extends { [key: string]: infer U } ? U : never;
type MyObject = ObjectType<{ name: string, age: number }>; // MyObject is of type {name:string, age: number}

Use the latest version of Typescript:

Typescript is an evolving language, and new versions often include important bug fixes and new features. Keeping your project up-to-date will ensure that you have access to the latest and best features.

Final Thoughts:

By following these best practices, you can take full advantage of Typescript's powerful features and write more robust, maintainable code.

It's also important to note that these are not the only best practices to follow, and it can vary depending on the project and its requirements. But by following these basic principles, you'll be in.