Chinese | English
Applying the concept of Clean Code to TypeScript is inspired by clean-code-javascript.
This article is translated from labs42io/clean-code-typescript.
directory
- Introduction to the
- variable
- function
- Objects and data structures
- class
- The principle of SOLID
- test
- concurrent
- Error handling
- formatting
- annotation
Introduction to the
This is not a TypeScript design specification. Rather, it applies Robert C. Martin’s software engineering book Clean Code to TypeScript to guide readers in writing easy-to-read, reusable, and reconfigurable software in TypeScript.
Not every principle is strictly adhered to, and even fewer are widely agreed upon. While this is just a guideline, it is a distillation of years of programming experience by Clean Code’s authors.
Software engineering has been around for more than 50 years, and we still have a lot to learn. When software architecture is as old as architecture itself, perhaps we have stricter rules to follow. Now, let these guidelines serve as a litmus test for evaluating the quality of your code and that of your team.
Also, understanding these principles won’t immediately make you a good programmer, nor will it mean years of working without making mistakes. Every piece of code starts from an imperfect point of view and continues to improve, just like clay becomes pottery. Enjoy the process!
variable
Variable names should make sense
Make meaningful distinctions to make it easier for readers to understand the meaning of variables.
Example:
function between<T> (a1: T, a2: T, a3: T) {
return a2 <= a1 && a1 <= a3;
}
Copy the code
Is:
function between<T> (value: T, left: T, right: T) {
return left <= value && value <= right;
}
Copy the code
Variable names can be spelled out
If you can’t read it, you’ll sound like an idiot talking about it.
Example:
class DtaRcrd102 {
private genymdhms: Date;
private modymdhms: Date;
private pszqint = '102';
}
Copy the code
Is:
class Customer {
private generationTimestamp: Date;
private modificationTimestamp: Date;
private recordId = '102';
}
Copy the code
Use uniform names for variables that have the same function
Example:
function getUserInfo() :User;
function getUserDetails() :User;
function getUserData() :User;
Copy the code
Is:
function getUser() :User;
Copy the code
Use searchable names
We read more code than we write, so readability and retrievability are very important. If you don’t extract and name variable names that make sense, you cheat the person reading the code. For code to be retrievable, TSLint helps identify unnamed constants.
Example:
// What the heck is 86400000 for?
setTimeout(restart, 86400000);
Copy the code
Is:
// Declare them as capitalized named constants.
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000;
setTimeout(restart, MILLISECONDS_IN_A_DAY);
Copy the code
Use self-explanatory variable names
Example:
declare const users:Map<string, User>;
for (const keyValue of users) {
// iterate through users map
}
Copy the code
Is:
declare const users:Map<string, User>;
for (const [id, user] of users) {
// iterate through users map
}
Copy the code
Avoid mental mapping
Don’t let people guess or imagine the meaning of variables; clarity is king.
Example:
const u = getUser();
const s = getSubscription();
const t = charge(u, s);
Copy the code
Is:
const user = getUser();
const subscription = getSubscription();
const transaction = charge(user, subscription);
Copy the code
Do not add useless context
If the class name or object name already expresses some information, do not repeat it in the internal variable name.
Example:
type Car = {
carMake: string;
carModel: string;
carColor: string;
}
function print(car: Car) :void {
console.log(`${this.carMake} ${this.carModel} (${this.carColor}) `);
}
Copy the code
Is:
type Car = {
make: string;
model: string;
color: string;
}
function print(car: Car) :void {
console.log(`${this.make} ${this.model} (${this.color}) `);
}
Copy the code
Use default parameters instead of short circuit or condition judgments
In general, the default parameters are neater than short-circuiting.
Example:
function loadPages(count: number) {
constloadCount = count ! = =undefined ? count : 10;
// ...
}
Copy the code
Is:
function loadPages(count: number = 10) {
// ...
}
Copy the code
function
As few parameters as possible (ideally no more than 2)
Limit the number of arguments so that function testing is easier. More than three parameters can lead to a surge in test complexity, requiring many different parameter combinations to be tested. Ideally, only one or two parameters. If you have more than two arguments, your function may be too complex.
If you need a lot of parameters, consider using objects. To make the properties of functions clearer, destruct can be used, which has the following advantages:
-
When someone looks at the function signature, it is immediately clear which attributes are used.
-
Destructuring makes a deep copy of the argument object passed to the function, which prevents side effects. (Note: objects and arrays deconstructed from parameter objects are not cloned)
-
TypeScript displays warnings for unused properties.
Example:
function createMenu(title: string, body: string, buttonText: string, cancellable: boolean) {
// ...
}
createMenu('Foo'.'Bar'.'Baz'.true);
Copy the code
Is:
function createMenu(options: {title: string, body: string, buttonText: string, cancellable: boolean}) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
Copy the code
TypeScript type aliases further improve readability.
type MenuOptions = {title: string, body: string, buttonText: string, cancellable: boolean};
function createMenu(options: MenuOptions) {
// ...
}
createMenu({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
});
Copy the code
Just do one thing
This is by far the most important rule in software engineering. If a function does more than one thing, it is harder to compose, test, and understand. Conversely, a function has only one behavior, which makes it easier to refactor and the code cleaner. If that’s all you know from this guide, you’re ahead of most programmers.
Example:
function emailClients(clients: Client) {
clients.forEach((client) = > {
const clientRecord = database.lookup(client);
if(clientRecord.isActive()) { email(client); }}); }Copy the code
Is:
function emailClients(clients: Client) {
clients.filter(isActiveClient).forEach(email);
}
function isActiveClient(client: Client) {
const clientRecord = database.lookup(client);
return clientRecord.isActive();
}
Copy the code
Worthy of the name
The function name shows what the function does.
Example:
function addToDate(date: Date, month: number) :Date {
// ...
}
const date = new Date(a);// It's hard to tell from the function name what is added
addToDate(date, 1);
Copy the code
Is:
function addMonthToDate(date: Date, month: number) :Date {
// ...
}
const date = new Date(a); addMonthToDate(date,1);
Copy the code
Each function contains only one level of abstraction
When there are multiple levels of abstraction, the function should be doing too much. Splitting up functions for reuse also makes testing easier.
Example:
function parseCode(code:string) {
const REGEXES = [ / *... * / ];
const statements = code.split(' ');
const tokens = [];
REGEXES.forEach((regex) = > {
statements.forEach((statement) = > {
// ...
});
});
const ast = [];
tokens.forEach((token) = > {
// lex...
});
ast.forEach((node) = > {
// parse...
});
}
Copy the code
Is:
const REGEXES = [ / *... * / ];
function parseCode(code:string) {
const tokens = tokenize(code);
const syntaxTree = parse(tokens);
syntaxTree.forEach((node) = > {
// parse...
});
}
function tokenize(code: string) :Token[] {
const statements = code.split(' ');
const tokens:Token[] = [];
REGEXES.forEach((regex) = > {
statements.forEach((statement) = > {
tokens.push( / *... * / );
});
});
return tokens;
}
function parse(tokens: Token[]) :SyntaxTree {
const syntaxTree:SyntaxTree[] = [];
tokens.forEach((token) = > {
syntaxTree.push( / *... * / );
});
return syntaxTree;
}
Copy the code
Delete duplicate code
Repetition is the root of all evil! Repetition means that if you want to change one piece of logic, you need to change multiple pieces of code :cry:. Imagine running a restaurant and keeping track of your inventory: all the tomatoes, Onions, garlic, spices, etc. How painful it is to maintain multiple inventory lists!
Duplicate code exists because there are two or more functions that are very similar, with one difference, but that difference forces you to use multiple independent functions to do many of the same things. Removing duplicate code means creating an abstraction that can handle this different set of things with just one function/module/class.
Sound abstraction is crucial, which is why you should follow the SOLID principle. Bad abstractions can be worse than repetitive code, so be careful! Having said that, do a good job of abstraction! Try not to repeat.
Example:
function showDeveloperList(developers: Developer[]) {
developers.forEach((developer) = > {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const githubLink = developer.getGithubLink();
const data = {
expectedSalary,
experience,
githubLink
};
render(data);
});
}
function showManagerList(managers: Manager[]) {
managers.forEach((manager) = > {
const expectedSalary = manager.calculateExpectedSalary();
const experience = manager.getExperience();
const portfolio = manager.getMBAProjects();
const data = {
expectedSalary,
experience,
portfolio
};
render(data);
});
}
Copy the code
Is:
class Developer {
// ...
getExtraDetails() {
return {
githubLink: this.githubLink,
}
}
}
class Manager {
// ...
getExtraDetails() {
return {
portfolio: this.portfolio,
}
}
}
function showEmployeeList(employee: Developer | Manager) {
employee.forEach((employee) = > {
const expectedSalary = developer.calculateExpectedSalary();
const experience = developer.getExperience();
const extra = employee.getExtraDetails();
const data = {
expectedSalary,
experience,
extra,
};
render(data);
});
}
Copy the code
Sometimes there is a trade-off between repeating code and introducing unnecessary abstractions that add complexity. When two different modules come from different domains and their implementations look similar, copying is acceptable and a little better than extracting common code. Because extracting common code leads to an indirect dependency between the two modules.
useObject.assign
ordeconstruction
To set the default object
Example:
typeMenuConfig = {title? :string, body? :string, buttonText? :string, cancellable? :boolean};
function createMenu(config: MenuConfig) {
config.title = config.title || 'Foo';
config.body = config.body || 'Bar';
config.buttonText = config.buttonText || 'Baz'; config.cancellable = config.cancellable ! = =undefined ? config.cancellable : true;
}
const menuConfig = {
title: null,
body: 'Bar',
buttonText: null,
cancellable: true
};
createMenu(menuConfig);
Copy the code
Is:
typeMenuConfig = {title? :string, body? :string, buttonText? :string, cancellable? :boolean};
function createMenu(config: MenuConfig) {
const menuConfig = Object.assign({
title: 'Foo',
body: 'Bar',
buttonText: 'Baz',
cancellable: true
}, config);
}
createMenu({ body: 'Bar' });
Copy the code
Alternatively, you can use the default destruct:
type MenuConfig = {title? : string, body? : string, buttonText? : string, cancellable? : boolean}; function createMenu({title = 'Foo', body = 'Bar', buttonText = 'Baz', cancellable = true}: MenuConfig) { // ... } createMenu({ body: 'Bar' });Copy the code
To avoid side effects, it is not allowed to pass undefined or NULL explicitly. See the — strictnullCheck option in the TypeScript compiler.
Do not use Flag arguments
The Flag argument tells the user that the function does more than one thing. If the function uses booleans to implement different code logical paths, consider splitting them.
Example:
function createFile(name:string, temp:boolean) {
if (temp) {
fs.create(`./temp/${name}`);
} else{ fs.create(name); }}Copy the code
Is:
function createFile(name:string) {
fs.create(name);
}
function createTempFile(name:string) {
fs.create(`./temp/${name}`);
}
Copy the code
Avoid side effects (part1)
A function is said to have side effects when it produces behavior other than “one input, one output.” Such as writing a file, changing global variables, or giving all your money to a stranger.
In some cases, the program requires some side effects. As in the previous example of writing files, you should keep these functions together and not modify a file with multiple functions/classes. Accomplish this requirement with one and only one Service.
The point is to avoid common pitfalls, such as sharing state between unstructured objects, using mutable data types, and not knowing where side effects occur. If you can do this, you may have the last laugh!
Example:
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
let name = 'Robert C. Martin';
function toBase64() {
name = btoa(name);
}
toBase64(); // produces side effects to `name` variable
console.log(name); // expected to print 'Robert C. Martin' but instead 'Um9iZXJ0IEMuIE1hcnRpbg=='
Copy the code
Is:
// Global variable referenced by following function.
// If we had another function that used this name, now it'd be an array and it could break it.
const name = 'Robert C. Martin';
function toBase64(text:string) :string {
return btoa(text);
}
const encodedName = toBase64(name);
console.log(name);
Copy the code
Avoid side effects (part2)
In JavaScript, primitive types are passed by value, and objects and arrays are passed by reference.
There is a case where if your function modifies the cart array to add purchased items, all other functions that use the CART array will be affected by the addition. Imagine a bad situation:
The user clicks the “Buy” button, which invokes the Purchase function, which requests the network and sends the CART array to the server. Because of the poor network connection, the purchase function must continually retry the request. What if a user accidentally clicks the “Add to Cart” button on an unwanted item just before the web request starts? When the network request starts, the Purchase function sends the unexpected addition because it references a shopping cart array that the addItemToCart function modifies to add unwanted items.
A good solution is for addItemToCart to always clone the Cart, edit it, and return the clone. This ensures that other functions that reference the shopping cart are not affected by any changes.
Two points to note:
-
In some rare cases, you might actually want to modify the input object. And most can be reconstructed to ensure no side effects! (See pure function)
-
In terms of performance, cloning large objects is really expensive. Fortunately, there are some good libraries that provide fast and efficient ways to do this without the memory footprint of manually cloning objects and arrays.
Example:
function addItemToCart(cart: CartItem[], item:Item) :void {
cart.push({ item, date: Date.now() });
};
Copy the code
Is:
function addItemToCart(cart: CartItem[], item:Item) :CartItem[] {
return [...cart, { item, date: Date.now() }];
};
Copy the code
Don’t write global functions
Polluting globally in JavaScript is very bad, and it can cause conflicts with other libraries that the user calling your API doesn’t know about until he or she gets an exception in the actual environment.
Consider this example: What if you wanted to extend a JavaScript Array to have a diff method that shows the difference between two arrays? You could write the new function to array.prototype, but it might conflict with another library that is trying to do the same thing. What if another library just uses diff to find the difference between the first and last element of an array?
A better approach is to extend Array to implement the corresponding functions.
Example:
declare global {
interface Array<T> {
diff(other: T[]): Array<T>; }}if (!Array.prototype.diff){
Array.prototype.diff = function <T> (other: T[]) :T[] {
const hash = new Set(other);
return this.filter(elem= >! hash.has(elem)); }; }Copy the code
Is:
class MyArray<T> extends Array<T> {
diff(other: T[]): T[] {
const hash = new Set(other);
return this.filter(elem= >! hash.has(elem)); }; }Copy the code
Functional programming is better than imperative programming
Use functional programming whenever possible!
Example:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000}];let totalOutput = 0;
for (let i = 0; i < contributions.length; i++) {
totalOutput += contributions[i].linesOfCode;
}
Copy the code
Is:
const contributions = [
{
name: 'Uncle Bobby',
linesOfCode: 500
}, {
name: 'Suzie Q',
linesOfCode: 1500
}, {
name: 'Jimmy Gosling',
linesOfCode: 150
}, {
name: 'Gracie Hopper',
linesOfCode: 1000}];const totalOutput = contributions
.reduce((totalLines, output) = > totalLines + output.linesOfCode, 0)
Copy the code
Package judgment condition
Example:
if (subscription.isTrial || account.balance > 0) {
// ...
}
Copy the code
Is:
function canActivateService(subscription: Subscription, account: Account) {
return subscription.isTrial || account.balance > 0
}
if (canActivateService(subscription, account)) {
// ...
}
Copy the code
Avoid “negative” judgments
Example:
function isEmailNotUsed(email: string) {
// ...
}
if (isEmailNotUsed(email)) {
// ...
}
Copy the code
Is:
function isEmailUsed(email) {
// ...
}
if(! isEmailUsed(node)) {// ...
}
Copy the code
Avoid judgment condition
It seems impossible to pull this off. The first thing most people think when they hear this is, “How can I function without an if statement?” In most cases, you can use polymorphism to achieve the same functionality. The next question is “Why?” The reason is the same as before: functions only do one thing.
Example:
class Airplane {
private type: string;
// ...
getCruisingAltitude() {
switch (this.type) {
case '777':
return this.getMaxAltitude() - this.getPassengerCount();
case 'Air Force One':
return this.getMaxAltitude();
case 'Cessna':
return this.getMaxAltitude() - this.getFuelExpenditure();
default:
throw new Error('Unknown airplane type.'); }}}Copy the code
Is:
class Airplane {
// ...
}
class Boeing777 extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getPassengerCount(); }}class AirForceOne extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude(); }}class Cessna extends Airplane {
// ...
getCruisingAltitude() {
return this.getMaxAltitude() - this.getFuelExpenditure(); }}Copy the code
Avoiding type checking
TypeScript is a strict syntactic superset of JavaScript with static type checking features. So specifying the types of variables, parameters, and return values to take advantage of this feature makes refactoring easier.
Example:
function travelToTexas(vehicle: Bicycle | Car) {
if (vehicle instanceof Bicycle) {
vehicle.pedal(this.currentLocation, new Location('texas'));
} else if (vehicle instanceof Car) {
vehicle.drive(this.currentLocation, new Location('texas')); }}Copy the code
Is:
type Vehicle = Bicycle | Car;
function travelToTexas(vehicle: Vehicle) {
vehicle.move(this.currentLocation, new Location('texas'));
}
Copy the code
Don’t over-optimize
Modern browsers do a lot of low-level optimizations at run time. A lot of times, you’re just wasting your time optimizing. There are some great resources to help locate where optimization is needed, find it, and fix it.
Example:
// On old browsers, each iteration with uncached `list.length` would be costly
// because of `list.length` recomputation. In modern browsers, this is optimized.
for (let i = 0, len = list.length; i < len; i++) {
// ...
}
Copy the code
Is:
for (let i = 0; i < list.length; i++) {
// ...
}
Copy the code
Delete useless code
Useless code and duplicate code need not be retained. If there is no place to call it, remove it! If you still need it, you can look at the version history.
Example:
function oldRequestModule(url: string) {
// ...
}
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Copy the code
Is:
function requestModule(url: string) {
// ...
}
const req = requestModule;
inventoryTracker('apples', req, 'www.inventory-awesome.io');
Copy the code
Use iterators and generators
Use generators and iterators when working with collections of data as if they were streams.
The reasons are as follows:
- The caller is decoupled from the generator, in the sense that the caller decides how many items to access.
- Delay execution and use as needed.
- Built-in support
for-of
Syntax iteration - Allows the implementation of optimized iterator patterns
Example:
function fibonacci(n: number) :number[] {
if (n === 1) return [0];
if (n === 2) return [0.1];
const items: number[] = [0.1];
while (items.length < n) {
items.push(items[items.length - 2] + items[items.length - 1]);
}
return items;
}
function print(n: number) {
fibonacci(n).forEach(fib= > console.log(fib));
}
// Print first 10 Fibonacci numbers.
print(10);
Copy the code
Is:
// Generates an infinite stream of Fibonacci numbers.
// The generator doesn't keep the array of all numbers.
function* fibonacci() :IterableIterator<number> {
let [a, b] = [0.1];
while (true) {
yielda; [a, b] = [b, a + b]; }}function print(n: number) {
let i = 0;
for (const fib in fibonacci()) {
if (i++ === n) break;
console.log(fib); }}// Print first 10 Fibonacci numbers.
print(10);
Copy the code
Some libraries handle iterations in a similar way to native arrays by linking “map,” “slice,” “forEach,” and so on. See ITIRIRI for examples of advanced operations that use iterators (or itiRIri-async for asynchronous iterations).
import itiriri from 'itiriri';
function* fibonacci() :IterableIterator<number> {
let [a, b] = [0.1];
while (true) {
yield a;
[a, b] = [b, a + b];
}
}
itiriri(fibonacci())
.take(10)
.forEach(fib= > console.log(fib));
Copy the code
Objects and data structures
usegetters
andsetters
TypeScript supports getter/setter syntax. Using getters and setters to access data from an object is better than simply looking up properties on the object. Here’s why:
- You don’t have to look up and modify every accessor in your code when you need to do something before you get an object property.
- perform
set
It is easier to add validation when. - Encapsulate the internal representation.
- Easier to add logging and error handling.
- You can lazily load an object’s properties, such as fetching it from the server.
Example:
class BankAccount {
balance: number = 0;
// ...
}
const value = 100;
const account = new BankAccount();
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
account.balance = value;
Copy the code
Is:
class BankAccount {
private accountBalance: number = 0;
get balance(): number {
return this.accountBalance;
}
set balance(value: number) {
if (value < 0) {
throw new Error('Cannot set negative balance.');
}
this.accountBalance = value;
}
// ...
}
const account = new BankAccount();
account.balance = 100;
Copy the code
Let objects have private/protected members
TypeScript class members support public(default), protected, and private access restrictions.
Example:
class Circle {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
perimeter(){
return 2 * Math.PI * this.radius;
}
surface(){
return Math.PI * this.radius * this.radius; }}Copy the code
Is:
class Circle {
constructor(private readonly radius: number) {
}
perimeter(){
return 2 * Math.PI * this.radius;
}
surface(){
return Math.PI * this.radius * this.radius; }}Copy the code
invariance
The TypeScript type system allows you to make individual properties on interfaces and classes read-only and run as functions.
As an advanced scenario, you can use the built-in type Readonly, which accepts type T and marks all of its properties as read-only using the mapping type.
Example:
interface Config {
host: string;
port: string;
db: string;
}
Copy the code
Is:
interface Config {
readonly host: string;
readonly port: string;
readonly db: string;
}
Copy the code
Type vs Interface
Use types when you might want to join or intersect. If you need extensions or implementations, use interfaces. However, there are no hard and fast rules, only applicable rules.
See Typescript for an explanation of the difference between Type and interface.
Example:
interface EmailConfig {
// ...
}
interface DbConfig {
// ...
}
interface Config {
// ...
}
/ /...
type Shape {
// ...
}
Copy the code
Is:
type EmailConfig {
// ...
}
type DbConfig {
// ...
}
type Config = EmailConfig | DbConfig;
// ...
interface Shape {
}
class Circle implements Shape {
// ...
}
class Square implements Shape {
// ...
}
Copy the code
class
Small, small, small! Tell the story three times
The size of a class is measured by its responsibilities. In accordance with the single responsibility principle, the class should be small.
Example:
class Dashboard {
getLanguage(): string { / *... * / }
setLanguage(language: string) :void { / *... * / }
showProgress(): void { / *... * / }
hideProgress(): void { / *... * / }
isDirty(): boolean { / *... * / }
disable(): void { / *... * / }
enable(): void { / *... * / }
addSubscription(subscription: Subscription): void { / *... * / }
removeSubscription(subscription: Subscription): void { / *... * / }
addUser(user: User): void { / *... * / }
removeUser(user: User): void { / *... * / }
goToHomePage(): void { / *... * / }
updateProfile(details: UserDetails): void { / *... * / }
getVersion(): string { / *... * / }
// ...
}
Copy the code
Is:
class Dashboard {
disable(): void { / *... * / }
enable(): void { / *... * / }
getVersion(): string { / *... * /}}// split the responsibilities by moving the remaining methods to other classes
// ...
Copy the code
High cohesion and low coupling
Cohesion: Defines the degree to which members of a class are related to each other. Ideally, every method with high cohesion should use all the fields in the class, which is neither possible nor desirable. But we still advocate high cohesion.
Coupling: Refers to the degree to which two classes are related. If a change in one class does not affect the other, it is called a low-coupling class.
Good software design has high cohesion and low coupling.
Example:
class UserManager {
// Bad: each private variable is used by one or another group of methods.
// It makes clear evidence that the class is holding more than a single responsibility.
// If I need only to create the service to get the transactions for a user,
// I'm still forced to pass and instance of emailSender.
constructor(
private readonly db: Database,
private readonly emailSender: EmailSender) {}async getUser(id: number) :Promise<User> {
return await db.users.findOne({ id })
}
async getTransactions(userId: number) :Promise<Transaction[]> {
return await db.transactions.find({ userId })
}
async sendGreeting(): Promise<void> {
await emailSender.send('Welcome! ');
}
async sendNotification(text: string) :Promise<void> {
await emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...}}Copy the code
Is:
class UserService {
constructor(private readonly db: Database) {}async getUser(id: number) :Promise<User> {
return await db.users.findOne({ id })
}
async getTransactions(userId: number) :Promise<Transaction[]> {
return await db.transactions.find({ userId })
}
}
class UserNotifier {
constructor(private readonly emailSender: EmailSender) {}async sendGreeting(): Promise<void> {
await emailSender.send('Welcome! ');
}
async sendNotification(text: string) :Promise<void> {
await emailSender.send(text);
}
async sendNewsletter(): Promise<void> {
// ...}}Copy the code
Composition over Inheritance
As the Gang of Four points out in design patterns, you use composition rather than inheritance whenever possible. Both composition and inheritance have their advantages and disadvantages. The main point of this maxim is that if you are subconsciously inclined towards inheritance, try to think about whether combinations can better model your problem, and in some cases they can.
When should inheritance be used? It depends on the problem you are facing. Inheritance is better for the following scenarios:
- Inheritance represents an “IS-A” relationship, not a “has-A” relationship (people -> animals vs. User -> User Details).
- Code for reusable base classes (humans can move like all animals).
- You want to make global changes to the derived classes by changing the base class (changing the caloric expenditure of all animals while moving).
Example:
class Employee {
constructor(
private readonly name: string.private readonly email:string) {}// ...
}
// Bad because Employees "have" tax data. EmployeeTaxData is not a type of Employee
class EmployeeTaxData extends Employee {
constructor(
name: string,
email:string.private readonly ssn: string.private readonly salary: number) {
super(name, email);
}
// ...
}
Copy the code
Is:
class Employee {
private taxData: EmployeeTaxData;
constructor(
private readonly name: string.private readonly email:string) {
}
setTaxData(ssn: string, salary: number): Employee {
this.taxData = new EmployeeTaxData(ssn, salary);
return this;
}
// ...
}
class EmployeeTaxData {
constructor(
public readonly ssn: string.public readonly salary: number) {}// ...
}
Copy the code
Method chain
Very useful pattern, found in many libraries. It makes code more expressive and concise.
Example:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string) :void {
this.collection = collection;
}
page(number: number, itemsPerPage: number = 100) :void {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage; } orderBy(... fields:string[]) :void {
this.orderByFields = fields;
}
build(): Query {
// ...}}// ...
const query = new QueryBuilder();
query.from('users');
query.page(1.100);
query.orderBy('firstName'.'lastName');
const query = queryBuilder.build();
Copy the code
Is:
class QueryBuilder {
private collection: string;
private pageNumber: number = 1;
private itemsPerPage: number = 100;
private orderByFields: string[] = [];
from(collection: string) :this {
this.collection = collection;
return this;
}
page(number: number, itemsPerPage: number = 100) :this {
this.pageNumber = number;
this.itemsPerPage = itemsPerPage;
return this; } orderBy(... fields:string[]) :this {
this.orderByFields = fields;
return this;
}
build(): Query {
// ...}}// ...
const query = new QueryBuilder()
.from('users')
.page(1.100)
.orderBy('firstName'.'lastName')
.build();
Copy the code
The principle of SOLID
Single Responsibility Principle (SRP)
As Clean Code states, “There should be no more than one reason for a class to change.” It seems tempting to pack a lot of functionality into a single class, like you can only bring one suitcase on a flight. The problem with this is that classes are conceptually not cohesive, and there are many reasons to modify classes. We should minimize the number of class changes. If a class has too many features, modifying one of them can be difficult to determine the impact on other dependent modules in the code base.
Example:
class UserSettings {
constructor(private readonly user: User) {
}
changeSettings(settings: UserSettings) {
if (this.verifyCredentials()) {
// ...
}
}
verifyCredentials() {
// ...}}Copy the code
Is:
class UserAuth {
constructor(private readonly user: User) {
}
verifyCredentials() {
// ...}}class UserSettings {
private readonly auth: UserAuth;
constructor(private readonly user: User) {
this.auth = new UserAuth(user);
}
changeSettings(settings: UserSettings) {
if (this.auth.verifyCredentials()) {
// ...}}}Copy the code
Open close Principle (OCP)
As Bertrand Meyer says, “Software entities (classes, modules, functions, etc.) should be open to extension and closed to modification.” In other words, it allows you to add new functionality without changing existing code.
Example:
class AjaxAdapter extends Adapter {
constructor() {
super(a); }// ...
}
class NodeAdapter extends Adapter {
constructor() {
super(a); }// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {}async fetch<T>(url: string) :Promise<T> {
if (this.adapter instanceof AjaxAdapter) {
const response = await makeAjaxCall<T>(url);
// transform response and return
} else if (this.adapter instanceof NodeAdapter) {
const response = await makeHttpCall<T>(url);
// transform response and return}}}function makeAjaxCall<T> (url: string) :Promise<T> {
// request and return promise
}
function makeHttpCall<T> (url: string) :Promise<T> {
// request and return promise
}
Copy the code
Is:
abstract class Adapter {
abstract async request<T>(url: string) :Promise<T>;
}
class AjaxAdapter extends Adapter {
constructor() {
super(a); }async request<T>(url: string) :Promise<T>{
// request and return promise
}
// ...
}
class NodeAdapter extends Adapter {
constructor() {
super(a); }async request<T>(url: string) :Promise<T>{
// request and return promise
}
// ...
}
class HttpRequester {
constructor(private readonly adapter: Adapter) {}async fetch<T>(url: string) :Promise<T> {
const response = await this.adapter.request<T>(url);
// transform response and return}}Copy the code
Richter’s Substitution Principle (LSP)
It’s a terrible term for a very simple concept.
Its formal definition is: “If S is a subtype of T, then an object of type T can be replaced with an object of type S without changing any desired properties of the program (correctness, task performed, etc.).” This is an even scarier definition.
A better explanation is that if you have a parent and a subclass, the parent and subclass can be used interchangeably without problems. This can still be confusing, so let’s take a look at the classic square rectangle example. Mathematically, a square is a rectangle, but if you model it using an “IS-A” relationship through inheritance, you quickly run into trouble.
Example:
class Rectangle {
constructor(
protected width: number = 0,
protected height: number = 0) {
}
setColor(color: string) {
// ...
}
render(area: number) {
// ...
}
setWidth(width: number) {
this.width = width;
}
setHeight(height: number) {
this.height = height;
}
getArea(): number {
return this.width * this.height; }}class Square extends Rectangle {
setWidth(width: number) {
this.width = width;
this.height = width;
}
setHeight(height: number) {
this.width = height;
this.height = height; }}function renderLargeRectangles(rectangles: Rectangle[]) {
rectangles.forEach((rectangle) = > {
rectangle.setWidth(4);
rectangle.setHeight(5);
const area = rectangle.getArea(); // BAD: Returns 25 for Square. Should be 20.
rectangle.render(area);
});
}
const rectangles = [new Rectangle(), new Rectangle(), new Square()];
renderLargeRectangles(rectangles);
Copy the code
Is:
abstract class Shape {
setColor(color: string) {
// ...
}
render(area: number) {
// ...
}
abstract getArea(): number;
}
class Rectangle extends Shape {
constructor(
private readonly width = 0,
private readonly height = 0) {
super(a); } getArea():number {
return this.width * this.height; }}class Square extends Shape {
constructor(private readonly length: number) {
super(a); } getArea():number {
return this.length * this.length; }}function renderLargeShapes(shapes: Shape[]) {
shapes.forEach((shape) = > {
const area = shape.getArea();
shape.render(area);
});
}
const shapes = [new Rectangle(4.5), new Rectangle(4.5), new Square(5)];
renderLargeShapes(shapes);
Copy the code
Interface Isolation Principle (ISP)
“Customers should not be forced to rely on interfaces they don’t use.” This principle is closely related to the principle of single liability. This means that you should not design a large, all-encompassing abstraction, which would add to the customer’s burden of implementing methods they don’t need.
Example:
interface ISmartPrinter {
print();
fax();
scan();
}
class AllInOnePrinter implements ISmartPrinter {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...}}class EconomicPrinter implements ISmartPrinter {
print() {
// ...
}
fax() {
throw new Error('Fax not supported.');
}
scan() {
throw new Error('Scan not supported.'); }}Copy the code
Is:
interface IPrinter {
print();
}
interface IFax {
fax();
}
interface IScanner {
scan();
}
class AllInOnePrinter implements IPrinter, IFax, IScanner {
print() {
// ...
}
fax() {
// ...
}
scan() {
// ...}}class EconomicPrinter implements IPrinter {
print() {
// ...}}Copy the code
Dependency Inversion Principle
This principle has two main points:
- High-level modules should not depend on low-level modules; both should depend on abstractions.
- Abstraction does not depend on implementation; implementation should depend on abstraction.
This is hard to understand at first, but if you use Angular, you’ll see that this principle is implemented in the form of dependency injection (DI). Although the concept is different, DIP prevents a high-level module from knowing the details of its low-level modules and setting them up. It can do this through DI. A huge benefit of doing this is to reduce coupling between modules. Coupling is bad, and it makes code hard to refactor.
Dips are typically implemented using inversion of control (IoC) containers. For example: TypeScript’s IoC container, InversifyJs
Example:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
class XmlFormatter {
parse<T>(content: string): T {
// Converts an XML string to an object T}}class ReportReader {
// BAD: We have created a dependency on a specific request implementation.
// We should just have ReportReader depend on a parse method: `parse`
private readonly formatter = new XmlFormatter();
async read(path: string) :Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text); }}// ...
const reader = new ReportReader();
await report = await reader.read('report.xml');
Copy the code
Is:
import { readFile as readFileCb } from 'fs';
import { promisify } from 'util';
const readFile = promisify(readFileCb);
type ReportData = {
// ..
}
interface Formatter {
parse<T>(content: string): T;
}
class XmlFormatter implements Formatter {
parse<T>(content: string): T {
// Converts an XML string to an object T}}class JsonFormatter implements Formatter {
parse<T>(content: string): T {
// Converts a JSON string to an object T}}class ReportReader {
constructor(private readonly formatter: Formatter){
}
async read(path: string) :Promise<ReportData> {
const text = await readFile(path, 'UTF8');
return this.formatter.parse<ReportData>(text); }}// ...
const reader = new ReportReader(new XmlFormatter());
await report = await reader.read('report.xml');
// or if we had to read a json report:
const reader = new ReportReader(new JsonFormatter());
await report = await reader.read('report.json');
Copy the code
test
Testing is more important than shipping. If there are no tests or insufficient numbers, there is no guarantee that you won’t introduce problems every time you release code. What is enough testing? It’s up to the team, but having 100% coverage (all statements and branches) gives the team more confidence. All of this is based on a good testing framework and coverage tools.
There is no reason not to write tests. There are many excellent JS testing frameworks that support TypeScript. Find one that your team likes. Then write tests for each new feature/module. Great if you like test-driven development (TDD), where the emphasis is on ensuring that code coverage is met before developing any features or refactoring existing features.
The three Laws of TDD
- Do not write production code until you write unit tests that fail.
- You can only write unit tests that just fail, not compile.
- You can only write production code that is just enough to pass the current failing test.
F.I.R.S.T. guidelines
Clean testing should follow the following guidelines:
- Fast, testing should be Fast (feedback on business code problems in a timely manner).
- Independent, each test process should be Independent.
- Repeatable, the test should be Repeatable in any environment.
- Self validationTesting results should be specific because you are pregnantthroughorfailure.
- Timely, the test code should be written before the production code.
Single test each concept
Tests should also follow the single responsibility principle, making only one assertion per unit test.
Example:
import { assert } from 'chai';
describe('AwesomeDate'.(a)= > {
it('handles date boundaries'.(a)= > {
let date: AwesomeDate;
date = new AwesomeDate('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
date = new AwesomeDate('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
date = new AwesomeDate('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
Copy the code
Is:
import { assert } from 'chai';
describe('AwesomeDate'.(a)= > {
it('handles 30-day months'.(a)= > {
const date = new AwesomeDate('1/1/2015');
date.addDays(30);
assert.equal('1/31/2015', date);
});
it('handles leap year'.(a)= > {
const date = new AwesomeDate('2/1/2016');
date.addDays(28);
assert.equal('02/29/2016', date);
});
it('handles non-leap year'.(a)= > {
const date = new AwesomeDate('2/1/2015');
date.addDays(28);
assert.equal('03/01/2015', date);
});
});
Copy the code
The test case name should show its intent
When a test fails, the first sign that something is wrong may be its name.
Example:
describe('Calendar'.(a)= > {
it('2/29/2020'.(a)= > {
// ...
});
it('throws'.(a)= > {
// ...
});
});
Copy the code
Is:
describe('Calendar'.(a)= > {
it('should handle leap year'.(a)= > {
// ...
});
it('should throw when format is invalid'.(a)= > {
// ...
});
});
Copy the code
concurrent
Promises instead of calls back
Callbacks are not neat and lead to excessive nesting *(callback hell)*.
Some tools use callbacks to convert existing functions into promise objects:
- Node. Js see util. Promisify
- See pify, ES6-promisify for general use
Example:
import { get } from 'request'; import { writeFile } from 'fs'; function downloadPage(url: string, saveTo: string, callback: (error: Error, content? : string) => void){ get(url, (error, response) => { if (error) { callback(error); } else { writeFile(saveTo, response.body, (error) => { if (error) { callback(error); } else { callback(null, response.body); }}); } }) } downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin', 'article.html', (error, content) => { if (error) { console.error(error); } else { console.log(content); }});Copy the code
Is:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
function downloadPage(url: string, saveTo: string) :Promise<string> {
return get(url)
.then(response= > write(saveTo, response))
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'.'article.html')
.then(content= > console.log(content))
.catch(error= > console.error(error));
Copy the code
Promise provides some helper methods to make the code more concise:
methods | describe |
---|---|
Promise.resolve(value) |
Return a promise parsed from the incoming value. |
Promise.reject(error) |
Return a promise with a reason for the rejection. |
Promise.all(promises) |
Returns a new Promise, passed into the arrayeachA promise is not completed until it is returned, or the first promise is rejected. |
Promise.race(promises) |
Returns a new Promise, passed into the arrayaA promise resolves or rejects, and the returned promise resolves or rejects. |
Promise.all is especially useful when running tasks in parallel, and promise.race makes it easier to implement timeouts for promises.
Async/Await
比 Promises
better
With the async/await syntax, you can write cleaner, more understandable chained promise code. A function prefixes the async keyword, and JavaScript runtime suspends code execution on the await keyword (when using promises).
Example:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = util.promisify(writeFile);
function downloadPage(url: string, saveTo: string) :Promise<string> {
return get(url).then(response= > write(saveTo, response))
}
downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'.'article.html')
.then(content= > console.log(content))
.catch(error= > console.error(error));
Copy the code
Is:
import { get } from 'request';
import { writeFile } from 'fs';
import { promisify } from 'util';
const write = promisify(writeFile);
async function downloadPage(url: string, saveTo: string) :Promise<string> {
const response = await get(url);
await write(saveTo, response);
return response;
}
// somewhere in an async function
try {
const content = await downloadPage('https://en.wikipedia.org/wiki/Robert_Cecil_Martin'.'article.html');
console.log(content);
} catch (error) {
console.error(error);
}
Copy the code
Error handling
Throwing errors is a good thing! It indicates that the runtime has successfully identified errors in the program, letting you know by stopping function execution on the current stack, terminating the process (in Node.js), and printing stack information in the console.
throwError
Or usereject
JavaScript and TypeScript allow you to throw any object. Promises can also be rejected for any reason.
Throw syntax of type Error is recommended. Because your errors can be caught in high-level code with catch syntax. Capturing string messages there is messy and makes debugging more painful. For the same reason, you should use the Error type when rejecting promises.
Example:
function calculateTotal(items: Item[]) :number {
throw 'Not implemented.';
}
function get() :Promise<Item[] >{
return Promise.reject('Not implemented.');
}
Copy the code
Is:
function calculateTotal(items: Item[]) :number {
throw new Error('Not implemented.');
}
function get() :Promise<Item[] >{
return Promise.reject(new Error('Not implemented.'));
}
// or equivalent to:
async function get() :Promise<Item[] >{
throw new Error('Not implemented.');
}
Copy the code
The advantage of using the Error type is that the try/catch/finally syntax supports it, and implicitly all errors have a stack property, which is useful for debugging.
Also, TypeScript is easier in this area, even if it doesn’t throw syntax and returns custom error objects instead. Consider the following example:
type Failable<R, E> = {
isError: true;
error: E;
} | {
isError: false;
value: R;
}
function calculateTotal(items: Item[]) :Failable<number, 'empty'> {
if (items.length === 0) {
return { isError: true, error: 'empty' };
}
// ...
return { isError: false, value: 42 };
}
Copy the code
Please refer to the original text for detailed explanation.
Don’t forget to catch errors
Catching errors without handling them is actually not fixing them, and logging errors to the console (console.log) is not much better, as it is often lost in the console’s massive logs. If you write your code in a try/catch, that’s where an error is likely to occur, so you should consider doing something when an error occurs.
Example:
try {
functionThatMightThrow();
} catch (error) {
console.log(error);
}
// or even worse
try {
functionThatMightThrow();
} catch (error) {
// ignore error
}
Copy the code
Is:
import { logger } from './logging'
try {
functionThatMightThrow();
} catch (error) {
logger.log(error);
}
Copy the code
Don’t ignore promises that are rejected
For the same reason that an Error cannot be ignored in a try/catch.
Example:
getUser()
.then((user: User) = > {
return sendEmail(user.email, 'Welcome! ');
})
.catch((error) = > {
console.log(error);
});
Copy the code
Is:
import { logger } from './logging'
getUser()
.then((user: User) = > {
return sendEmail(user.email, 'Welcome! ');
})
.catch((error) = > {
logger.log(error);
});
// or using the async/await syntax:
try {
const user = await getUser();
await sendEmail(user.email, 'Welcome! ');
} catch (error) {
logger.log(error);
}
Copy the code
formatting
Like many of the rules here, nothing is hard and fast, nor is formatting. The point is not to argue about formatting, but to use automated tools for formatting. Debating formats is a waste of time and money for engineers. The general rule is to maintain consistent formatting rules.
There is a powerful tool for TypeScript called TSLint. It is a static analysis tool that helps you dramatically improve the readability and maintainability of your code. For use in the project, you can refer to the following TSLint configuration:
-
TSLint Config Standard – Standard format rules
-
TSLint Config Airbnb – Airbnb format rules
-
TSLint Clean Code – Inspired by Clean Code: A Handbook of Agile Software Craftsmanship.
-
TSLint react – React related Lint rules
-
TSLint + Prettier – Prettier code formatting the related Lint rule
-
ESLint rules for tslint-typescript ESLint
-
Immutable – Disables mutation rules in TypeScript
You can also refer to the source code for the TypeScript style guide and coding conventions.
Case consistency
Capitals can tell you a lot about variables, functions, and so on. These are subjective rules, and your team makes the choice. The key is to be consistent no matter what you choose.
Example:
const DAYS_IN_WEEK = 7;
const daysInMonth = 30;
const songs = ['Back In Black'.'Stairway to Heaven'.'Hey Jude'];
const Artists = ['ACDC'.'Led Zeppelin'.'The Beatles'];
function eraseDatabase() {}
function restore_database() {}
class animal {}
class Container {}
Copy the code
Is:
const DAYS_IN_WEEK = 7;
const DAYS_IN_MONTH = 30;
const SONGS = ['Back In Black'.'Stairway to Heaven'.'Hey Jude'];
const ARTISTS = ['ACDC'.'Led Zeppelin'.'The Beatles'];
function eraseDatabase() {}
function restoreDatabase() {}
class Animal {}
class Container {}
Copy the code
It is best to use PASCAL names for class, interface, type, and namespace names.
Variables, functions, and class members use “hump naming.”
The calling function and the called function should be placed next to each other
When functions call each other, they should be placed close together. It is best to write the caller above the caller. It’s like reading a newspaper. We all read from the top down, so does reading code.
Example:
class PerformanceReview {
constructor(private readonly employee: Employee) {}private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getManagerReview() {
const manager = this.lookupManager();
}
private getSelfReview() {
// ...}}const review = new PerformanceReview(employee);
review.review();
Copy the code
Is:
class PerformanceReview {
constructor(private readonly employee: Employee) {
}
review() {
this.getPeerReviews();
this.getManagerReview();
this.getSelfReview();
// ...
}
private getPeerReviews() {
const peers = this.lookupPeers();
// ...
}
private lookupPeers() {
return db.lookup(this.employee.id, 'peers');
}
private getManagerReview() {
const manager = this.lookupManager();
}
private lookupManager() {
return db.lookup(this.employee, 'manager');
}
private getSelfReview() {
// ...}}const review = new PerformanceReview(employee);
review.review();
Copy the code
Organize imports
Using clean and easy-to-read import statements, you can quickly see the dependencies of the current code. Import statements should follow the following:
Import
Statements should be arranged alphabetically and grouped.- Unused import statements should be deleted.
- Named imports must be in alphabetical order (for example:
import {A, B, C} from 'foo';
). - Import sources must be arranged alphabetically in groups. Such as:
import * as foo from 'a'; import * as bar from 'b';
- Import groups are separated by blank lines.
- The group is sorted as follows:
- Polyfills (e.g.
import 'reflect-metadata';
) - Node built-in modules (for example:
import fs from 'fs';
) - External modules (e.g.
import { query } from 'itiriri';
) - Internal modules (e.g.
import { UserService } from 'src/services/userService';
) - Modules in the parent directory (e.g.
import foo from '.. /foo'; import qux from '.. /.. /foo/qux';
) - Modules from the same or sibling directory (e.g.
import bar from './bar'; import baz from './bar/baz';
)
- Polyfills (e.g.
Example:
import { TypeDefinition } from '.. /types/typeDefinition';
import { AttributeTypes } from '.. /model/attribute';
import { ApiCredentials, Adapters } from './common/api/authorization';
import fs from 'fs';
import { ConfigPlugin } from './plugins/config/configPlugin';
import { BindingScopeEnum, Container } from 'inversify';
import 'reflect-metadata';
Copy the code
Is:
import 'reflect-metadata';
import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';
import { AttributeTypes } from '.. /model/attribute';
import { TypeDefinition } from '.. /types/typeDefinition';
import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';
Copy the code
Use typescript aliases
To create neat import statements, you can set the paths and baseUrl properties of the compiler options in tsconfig.json.
This avoids using long relative paths when importing.
Example:
import { UserService } from '.. /.. /.. /services/UserService';
Copy the code
Is:
import { UserService } from '@services/UserService';
Copy the code
// tsconfig.json."compilerOptions": {..."baseUrl": "src"."paths": {
"@services": ["services/*"]}... }...Copy the code
annotation
Writing comments means you can’t express yourself without comments, and it’s best to express yourself in code.
Don’t comment out bad code, rewrite it! — Brian W. Kernighan and P. J. Plaugher
The code interprets itself rather than using comments
Code is documentation.
Example:
// Check if subscription is active.
if (subscription.endDate > Date.now) { }
Copy the code
Is:
const isSubscriptionActive = subscription.endDate > Date.now;
if (isSubscriptionActive) { / *... * / }
Copy the code
Do not leave commented out code in the code base
One reason for versioning is to make old code history.
Example:
class User {
name: string;
email: string;
// age: number;
// jobPosition: string;
}
Copy the code
Is:
class User {
name: string;
email: string;
}
Copy the code
Don’t write notes like a journal
Remember, use version control! There is no need to keep useless code, commented out code, especially comments like diaries. Use git log to get the history.
Example:
/** * 2016-12-20: Removed monads, didn't understand them (RM) * 2016-10-01: Improved using special monads (JP) * 2016-02-03: Added type-checking (LI) * 2015-03-14: Implemented combine (JR) */
function combine(a:number, b:number) :number {
return a + b;
}
Copy the code
Is:
function combine(a:number, b:number) :number {
return a + b;
}
Copy the code
Avoid using comments to mark locations
They often mess with code. To keep code structured, functions and variables should be properly indented and formatted.
Alternatively, you can use an IDE that supports Code folding (see Visual Studio Code Code folding).
Example:
////////////////////////////////////////////////////////////////////////////////
// Client class
////////////////////////////////////////////////////////////////////////////////
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
////////////////////////////////////////////////////////////////////////////////
// public methods
////////////////////////////////////////////////////////////////////////////////
public describe(): string {
// ...
}
////////////////////////////////////////////////////////////////////////////////
// private methods
////////////////////////////////////////////////////////////////////////////////
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...}};Copy the code
Is:
class Client {
id: number;
name: string;
address: Address;
contact: Contact;
public describe(): string {
// ...
}
private describeAddress(): string {
// ...
}
private describeContact(): string {
// ...}};Copy the code
TODO comment
Use the // TODO annotation when you find yourself needing to leave a comment in your code to remind you of future improvements. Most ides provide special support for this type of annotation, and you can quickly browse through the entire TODO list.
However, remember that TODO annotations are not an excuse for bad code.
Example:
function getActiveSubscriptions() :Promise<Subscription[] >{
// ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date()}}); }Copy the code
Is:
function getActiveSubscriptions() :Promise<Subscription[] >{
// TODO: ensure `dueDate` is indexed.
return db.subscriptions.find({ dueDate: { $lte: new Date()}}); }Copy the code