Skip to main content

Coding conventions part 2

· 9 min read

Part 1 documents conventions that can be enforced or guided through IDE settings.

Part 2 here describes conventions that really depend on the developers following them. In particular, the conventions here are for JavaScript/TypeScript code.

Naming

There are two hard things in Computer Science: cache invalidation, naming things, and off-by-1 errors.

Leon Bambrick

General rule of thumb:

  • Names of functions/variables should be spelled with letters only.
  • Numbers in names should only be used for localised variables (i.e. variables that are used within a function), and only when it makes sense (e.g. data from two similar sources).
  • Words can be abbreviated if the spelling is too long. E.g. abbr in place of abbreviation
  • Never name anything with 2 or fewer letters unless the scope in which they are valid in is extremely small (e.g. in a for loop that doesn't require scrolling to see its entirety).
  • When an acronym is used, letters in the acronym after the first letter must be lowercase. E.g. textXml.
  • Casing for the first letter depends on context. Functions typically start with a lowercase (e.g. checkIpAddress) whereas classes typically start with an uppercase (e.g. HttpRequestHandler).

Functions/Methods

  • Use camelCasing.
  • Use imperative verbs.
  • Names should be no more than 4 words. E.g. queryUserEmail, checkNewUserPresence
  • Functions that are used withn the same module (i.e. not exported) as helper functions should be prefixed with an underscore. E.g. _extractTitle.

Variables

  • Use camelCasing.
  • Names should be no more than 3 words.
  • Private variables should be named starting with an underscore (_) when it makes sense to differentiate them (for example local variables that should be differentiated from class properties).

Imports

import statements should be placed at the top of the file. There needs to be a strong reason to place the import statements anywhere else than the top of the page.

If there is a docblock at the top of the file, the import statements should be placed after the docblock.

The statements should be grouped by the modules according to the following heuristics:

  • Each group should be separated by an empty line.
  • The first group should contain the import statements of public npm packages.
  • The second group should contain the import statements of private npm packages, if applicable.
  • The third group onwards should contain local import statements.
  • They should also be further grouped by the level of traversal upwards.
  • Within each group, the order of the import statements should be based on the alphabetical order of the packages/modules/files.

For example:

import { GetServerSideProps } from 'next';
import Link from 'next/link';
import { ReactElement } from 'react';
import { Grid } from 'react-bootstrap-icons';

import { PAGE_SETUP, TOKEN_TWITTER } from './constants';

import { logger } from '../lib/logger';

TypeScript

Non-null assertion operator

The non-null assertion operator (the exclamation mark used after a variable) must be accompanied by a comment when used. E.g.

// `where` is initialised before this line.
args.where!.year = { in: [2023] };

As an alternative to using this operator, consider if the following alternative works:

args.where = Object.assign({}, args.where, {
year: { in: [2023] },
});

OR

args.where = { ...args.where, {
year: { in: [2023] },
}}

Imports

import statements should be placed at the top of the file. There needs to be a strong reason to place the import statements anywhere else than the top of the page.

If there is a docblock at the top of the file, the import statements should be placed after the docblock.

The statements should be grouped by the modules according to the following heuristics:

  • Each group should be separated by an empty line.
  • The first group should contain the import statements of public npm packages.
  • The second group should contain the import statements of private npm packages, if applicable.
  • The third group onwards should contain local import statements.
  • They should also be further grouped by the level of traversal upwards.
  • Within each group, the order of the import statements should be based on the alphabetical order of the packages/modules/files.

For example:

import { GetServerSideProps } from 'next';
import Link from 'next/link';
import { ReactElement } from 'react';
import { Grid } from 'react-bootstrap-icons';

import { PAGE_SETUP, TOKEN_TWITTER } from './constants';

import { logger } from '../lib/logger';

TypeScript

Non-null assertion operator

The non-null assertion operator (the exclamation mark used after a variable) must be accompanied by a comment when used. E.g.

// `where` is initialised before this line.
args.where!.year = { in: [2023] };

As an alternative to using this operator, consider if the following alternative works:

args.where = Object.assign({}, args.where, {
year: { in: [2023] },
});

OR

args.where = { ...args.where, {
year: { in: [2023] },
}}

React

  • Components should be named using PascalCasing.
  • Names should be no more than 3 words. E.g. UsernameInput, Footer.
  • Use functional components in conjunction with hooks.
  • Types, constants and variables should be defined outside of the component if possible. If not, they should be placed at the top of the component code (see below).
  • Hooks should be placed at the start of the component definition and should be written in the following order: useRouter, useContext, useState, useRef, useReducer, useCallback, useEffect.
  • Custom hooks should be called after the other hooks.
  • Define functions and event handlers next.
  • Component code (code that is for the component and runs just once per render), if any, should then be placed before the return statement. Variables and constants should be defined at the start in this block.
  • Callback functions passed to useEffect should be named even though anonymous functions are accepted. This will make the purpose of the useEffect calls much more clearer. E.g.
useEffect(function loadList() {
...
}, []);

Miscellaneous

Constants

The concept of constant is different in JavaScript/TypeScript as compared to other languages. The const keyword is used to define a variable as not changeable. This can have different effects depending on whether the type is a primitive (like string) or a reference (pointing to an object).

Here "constant" refers to values that are fixed, primitive values. These values do not change for the life time of the scope that they are used in. These variables are named in uppercase letters. Words are separated by underscores. E.g.

const GRAVITY_ON_MOON = 1.62;

A constant should be named with no more than 4 words.

If the "constant" is not a primitive value (e.g. an array), it should not be named in uppercase letters as the values in the array/object can be changed either by intention or not.

ESLint for backend

The ESLint configuration file is as follows:

module.exports = {
env: {
browser: true,
es2021: true,
},
extends: 'standard-with-typescript',
overrides: [],
parser: '@typescript-eslint/parser',
parserOptions: {
ecmaVersion: 'latest',
sourceType: 'module',
},
plugins: ['@typescript-eslint'],
rules: {
'comma-dangle': 'off',
'@typescript-eslint/comma-dangle': 'error',
// https://futurestud.io/tutorials/how-to-allow-trailing-commas-comma-dangle-with-typescript-eslint
'@typescript-eslint/no-unused-vars': ['error', 'only-multiline'],
},
};

The file is generated using npx eslint --init (reference https://blog.logrocket.com/linting-typescript-eslint-prettier) and modified afterwards.

Note: If the project is scaffolded using public tools such as create-next-app, follow the ESLint configuration from the scaffold.

TSConfig

The tsconfig.json file differs between that for the Web app and the server.

Backend

{
"include": ["src"],
"exclude": ["node_modules"],
"extends": "@tsconfig/node-lts/tsconfig.json",
"compilerOptions": {
"allowJs": false,
"outDir": "./dist",
"noImplicitAny": true
}
}

The file is generated using npx tsc --init. It is complimented by the package @tsconfig/node-lts.

Web frontend

The tsconfig.json file is created using Vite (npm create vite@latest).

TypeScript 5 Notes

Starting from TypeScript 5.0, the option --verbatimModuleSyntax replaces --importsNotUsedAsValues and --preserveValueImports. This setting should be included in tsconfig.json with the value true.

Reference: https://www.typescriptlang.org/tsconfig#verbatimModuleSyntax

Commits

Check your Git username and email

All too often I see programmers, particularly junior ones who have been using the same computer since they learnt Git, use the same username and email address for every project because they've set it globally! This makes it hard to identify and track down changes in the future.

Just have a look at this:

commit 551421f5251f9b6c0cb4fb5a7752ce58349b0ef8
Author: Spartacus Grizzly <cheekybear98@gmail.com>
Date: Mon Mar 18 15:44:41 2024 +0800

How the hell is any one going to know who Spartacus Grizzly is? 🙄

If you're going to work on a company job, use a proper name and the company email address given to you!

We're not asking every developer to change the name and email address globally. Just use git config user.name and git config user.email to change the values for the project.

Never commit with an empty message.

The fact that this has to be listed is frustrating. 😤

Each commit should be targeted

Each commit should aim to add one feature, or to fix one thing only. Another way of saying this is to make each commit as small as possible.

Not only does this make it easy to review code commits, any issues can be more easily tracked down as well. Any necessary reverts can be done more easily as well.

Corrections should be made as amendments rather than new commits

Commits that are basically corrections for the previous commit should included as part of the original commit as far as possible.

A common occurrence for this situation is when a file is modified but not saved before the commit. Or a file that should be included in the commit but was not.

Rather than making a new commit for this, the file should be staged and included with the previous commit using git commit --amend.

However, this should only be done for commits that are not pushed to the cloud repository. If the commit has already been pushed, this will cause the remote branch to be inconsistent with your local branch, causing the commit history to be more messy than adding a new commit.