Previous Parts: Clean Code: Functions and Methods in TypeScript [Part 1] Clean Code: Naming and Code Composition in TypeScript [Part 2] Table of contents Classes a. Naming b. Encapsulation c. Classes instead of prototypes d. Method chaining e. Access modifier static Objects a. Fields b. Accessors Getters/setters Closures 1 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 = 'any@gmail.com'; In this example, fields are opened for modification: we can delete fields, or assign not valid values. It is not safe. userProfile The better way is to encapsulate some class fields and use to get value and to set value. As setter is a method we can make some checking if value is valid before assignment. getters setters // 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('any@gmail.com'); c. Classes instead of prototypes syntax is more concise and easier to understand. class // 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 and 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 at the end of every function, and you can chain further class methods onto it. jQuery Lodash. this // 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", "888@gmail.com", "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", "888@gmail.com", "888"); user.setCountry("France").setId("100").save(); e. Access modifier static Class methods should employ the use of , 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. this // 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. : 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 fields or delete fields. user 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; } }; 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 fields and give the ability to break our logic. userProfile 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('any@gmail.com'); 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 field public. name 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.