paint-brush
Clean Code: Classes and Objects in TypeScript [Part 3]by@alenaananich
805 reads
805 reads

Clean Code: Classes and Objects in TypeScript [Part 3]

by Alena AnanichOctober 30th, 2023
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

CNN.com will feature iReporter photos in a weekly Travel Snapshots gallery. Please submit your best shots of the U.S. for next week. Visit CNN.com/Travel next Wednesday for a new gallery of snapshots. Please share your best photos of the United States with CNN iReport.
featured image - Clean Code: Classes and Objects in TypeScript [Part 3]
Alena Ananich HackerNoon profile picture

Previous Parts:

  1. Classes

    a. Naming

    b. Encapsulation

    c. Classes instead of prototypes

    d. Method chaining

    e. Access modifier static

  2. Objects

    a. Fields

    b. Accessors

    1. Getters/setters
    2. Closures 1
    3. Closures 2


Classes

a. Naming

When you create a class you already set the context for the class in its name. It will help us to avoid unnecessary context for class methods.

// Bad
class UserRoleServive {
  getUserRole() {}
  checkUserRole() {}
}

// Better
class UserRoleServive {
  getRole() {}
  checkRole() {}
}


b. Encapsulation

// Bad
class MakeUserProfile {
  constructor() {
    this.email = null;
    this.phone = null;
    /..logic../
  }
}

const userProfile = new MakeUserProfile();
userProfile.email = '[email protected]';


In this example, userProfile fields are opened for modification: we can delete fields, or assign not valid values. It is not safe.


The better way is to encapsulate some class fields and use getters to get value and setters to set value. As setter is a method we can make some checking if value is valid before assignment.

// Better
class MakeUserProfile {
  
  private email = null;
  private phone = null;

  getEmail(): string {
    return this.email;
  }
  
  setEmail(emailVal: string): void {
    // check validity before assignment
    if (typeof emailVal === 'string' && isNotUsedBefore(emailVal)) {
        this.email = emailVal;
    }
  }
}

const userProfile = new MakeUserProfile();
userProfile.setEmail('[email protected]');


c. Classes instead of prototypes

class syntax is more concise and easier to understand.

// bad
function UserProfile(name) {
  this.name = name;
}

UserProfile.prototype.checkName = function() {
  /..logic../
};

// good
class UserProfile {
  constructor(name) {
     this.name = name;
  }

  checkName(): boolean {
    /..logic../
}


d. Method chaining

This pattern is very useful and you can see it in many libraries such as jQuery and Lodash. It allows your code to be expressive, and less verbose. For that reason, I say, use method chaining and take a look at how clean your code will be. In your class functions, simply return this at the end of every function, and you can chain further class methods onto it.


// Bad
class User {
  constructor(name: string, email: string, phone: string) {
    this.name = name;
    this.email = email;
    this.phone = phone;
  }

  setCountry(country: string): void {
    this.country = country;
  }

  setAddress(address: string): void {
    this.address = address;
  }

  setId(id: string): void {
    this.id = id;
  }

  save(): Observable<void> {
    /..logic../
  }
}

const user = new User("Tom", "[email protected]", "888");
user.setCountry("France");
user.setId("100");
user.save();


// Better
class User {
  constructor(name: string, email: string, phone: string) {
    this.name = name;
    this.email = email;
    this.phone = phone;
  }

  setCountry(country: string): IUser {
    this.country = country;
    return this;
  }

  setAddress(address: string): IUser {
    this.address = address;
    return this;
  }

  setId(id: string): IUser {
    this.id = id;
    return this;
  }

  save(): Observable<void> {
    /..logic../
  }
}

const user = new User("Tom", "[email protected]", "888");
user.setCountry("France").setId("100").save();


e. Access modifier static

Class methods should employ the use of this, or they should be transformed into static methods unless an external library or framework necessitates the utilization of particular non-static methods. If a method is designated as an instance method, it should imply that it functions differently based on the properties of the object it is called on.

// Bad
class User {
  notify(): void {
    console.log('User Loaded');
  }

// Better - static methods aren't expected to use this
class User {
  static notify(): void {
    console.log('User Loaded');
  }
}

User.notify();


Objects

a. Fields

Use bracket notation [] when accessing properties with a variable.

const user = {
  name: 'Tom',
  age: 28,
};

function getUserProp(prop) {
  return user[prop];
}

const age = getUserProp('age');


b. Accessors

In this case, we will take a look at very similar to Class encapsulation logic. If object fields are not defended from the wrong assignment or deletion our code becomes unsafe. Lets take a look at different approaches.


  1. Encapsulate object fields with getters and setters:
// Bad
const user = {
  email: null,
  phoe: null,
}

// assign unexpected value
user.email = [{}];

// delete field
delete user.email;

In this example, we can freely assign the wrong value to user fields or delete fields.


The solution will be to use getters and setters for fields. As setters are functions we also can make additional checks before assignment:

// Better
const user = {
  
  get email(): string {
    return this._email;
  },

  set email(value: string) {
    if (typeof value !== 'string') {
     return;
    }
    this._email = value;
  }
};


  1. Use closures when a function builds an object

    Sometimes we prefer to create function that built objects. In this case, we also need to defend it from unexpected behavior. Let’s take a look at bad example.

// Bad
function makeUserProfile(): UserProfile {
  /..logic../

  return {
    email: null,
    phone: null,
    // ...
  };
}

const userProfile = makeUserProfile();

// Let's assign unxpected value
userProfile.email = new Error();

// And delete this field
delete userProfile.email;


Here we also open userProfile fields and give the ability to break our logic.


Let’s improve with closures:

// Better
function makeUserProfile(): UserProfile {
  // make the field private
  let email = null;

  // getter for email field
  function getEmail(): string {
    return email;
  }

  // setter for email field
  function setEmail(emailVal: string): void {
    // check validity before assignment
    if (emailVal && isNotUsedBefore(emailVal)) {
        email = emailVal;
    }
  }

  return {
    getEmail,
    setEmail,
  };
}

const userProfile = makeUserProfile();
userProfile.setEmail('[email protected]');


  1. Another way to make closures
// Bad
const UserProfile = function(name) {
  this.name = name;
};

UserProfile.prototype.getName = function getName() {
  return this.name;
};

const userProfile = new UserProfile("Any user");
console.log(`User name: ${userProfile .getName()}`); // Any user: Any user
delete userProfile.name;
console.log(`User name: ${userProfile .getName()}`); // User name: undefined


In this example, we also make name field public.


Let’s improve with closures to prevent deletion:

// Better
function makeUserProfile(name: string) {
  return {
    getName(): string {
      return name;
    }
  };
}

const userProfile = new UserProfile("Any user");
console.log(`User name: ${userProfile.getName()}`); // User name: Any user
delete employee.name;
console.log(`User name: ${userProfile.getName()}`); // User name: Any user


Conclusion

In this article we considered code smells in Classes and Objects. In the next article, we will take a look at some problems in architecture and SOLID principles which will help to improve your code.