Formatting
Formatting is subjective. Like many rules herein, there is no hard and fast rule that you must follow. The main point is DO NOT ARGUE over formatting. There are tons of tools to automate this. Use one! It's a waste of time and money for engineers to argue over formatting. The general rule to follow is keep consistent formatting rules.
For TypeScript there is a powerful tool called ESLint. It's a static analysis tool that can help you improve dramatically the readability and maintainability of your code. There are ready to use ESLint configurations that you can reference in your projects:
ESLint Config Airbnb - Airbnb style guide
ESLint Base Style Config - a Set of Essential ESLint rules for JS, TS and React
ESLint + Prettier - lint rules for Prettier code formatter
Refer also to this great TypeScript StyleGuide and Coding Conventions source.
Migrating from TSLint to ESLint
If you are looking for help in migrating from TSLint to ESLint, you can check out this project: https://github.com/typescript-eslint/tslint-to-eslint-config
Use consistent capitalization
Capitalization tells you a lot about your variables, functions, etc. These rules are subjective, so your team can choose whatever they want. The point is, no matter what you all choose, just be consistent.
Bad:
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() {}
type animal = { /* ... */ }
type Container = { /* ... */ }
Good:
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'];
const discography = getArtistDiscography('ACDC');
const beatlesSongs = SONGS.filter((song) => isBeatlesSong(song));
function eraseDatabase() {}
function restoreDatabase() {}
type Animal = { /* ... */ }
type Container = { /* ... */ }
Prefer using PascalCase
for class, interface, type and namespace names.
Prefer using camelCase
for variables, functions and class members.
Prefer using capitalized SNAKE_CASE
for constants.
Function callers and callees should be close
If a function calls another, keep those functions vertically close in the source file. Ideally, keep the caller right above the callee. We tend to read code from top-to-bottom, like a newspaper. Because of this, make your code read that way.
Bad:
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();
Good:
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();
Organize imports
With clean and easy to read import statements you can quickly see the dependencies of current code. Make sure you apply following good practices for import
statements:
- Import statements should be alphabetized and grouped.
- Unused imports should be removed.
- Named imports must be alphabetized (i.e.
import {A, B, C} from 'foo';
) - Import sources must be alphabetized within groups, i.e.:
import * as foo from 'a'; import * as bar from 'b';
- Prefer using
import type
instead ofimport
when importing only types from a file to avoid dependency cycles, as these imports are erased at runtime - Groups of imports are delineated by blank lines.
- Groups must respect following order:
- Polyfills (i.e.
import 'reflect-metadata';
) - Node builtin modules (i.e.
import fs from 'fs';
) - external modules (i.e.
import { query } from 'itiriri';
) - internal modules (i.e
import { UserService } from 'src/services/userService';
) - modules from a parent directory (i.e.
import foo from '../foo'; import qux from '../../foo/qux';
) - modules from the same or a sibling's directory (i.e.
import bar from './bar'; import baz from './bar/baz';
)
- Polyfills (i.e.
Bad:
import { TypeDefinition } from '../types/typeDefinition';
import { AttributeTypes } from '../model/attribute';
import { Customer, Credentials } from '../model/types';
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';
Good:
import 'reflect-metadata';
import fs from 'fs';
import { BindingScopeEnum, Container } from 'inversify';
import { AttributeTypes } from '../model/attribute';
import { TypeDefinition } from '../types/typeDefinition';
import type { Customer, Credentials } from '../model/types';
import { ApiCredentials, Adapters } from './common/api/authorization';
import { ConfigPlugin } from './plugins/config/configPlugin';
Use typescript aliases
Create prettier imports by defining the paths and baseUrl properties in the compilerOptions section in the tsconfig.json
This will avoid long relative paths when doing imports.
Bad:
import { UserService } from '../../../services/UserService';
Good:
import { UserService } from '@services/UserService';
// tsconfig.json
...
"compilerOptions": {
...
"baseUrl": "src",
"paths": {
"@services": ["services/*"]
}
...
}
...