JavaScript Red Style Guide
red is life passion fire
Priorities
- Readability
- Crystal clear way to add and subtract functionality
- Minimal vocabulary
- Modern
Outline
- Features to use
- Features to avoid
- Features to never use
- Details
1. Features to use
general
Use trailing commas.
Prefer const
over let
, no var
.
Explicit type conversion with the functions String
, Boolean
, Number
, Array.from
, Object.fromEntries
.
return
, throw
as early as possible to avoid huge indentation levels.
Make every line of code independent if possible, do not use chain variable assignments.
function
Use an expression because:
- it works as assignment
- as parameter for another function
- as IIFE
- compatible with arrow and original function syntax
- ability to mark as constant
- easy to alias
- easy to decorate
- no hoisting
Ideally less than 4 parameters. If more consider using an objects and destructuring. Always use explicit return for arrow functions.
const x = function (a, b) {
return a - b;
};
const y = (a, b) => {
return a * b;
};
object life-cycle
Declare all fields as soon as possible even if they don't have a value yet, put the creator/constructor function first. A seal on the object returned by the constructor return Object.seal(hero)
) should not throw any error.
const createHero = ({ name }) => {
const hero = {
name,
hitPoints: 100,
location: [0, 0],
favouriteAttack: undefined,
};
return hero;
};
const moveHero = (hero, [x, y]) => {
const [pastX, pastY] = hero.location;
hero.location = [pastX + x, pastY + y];
};
const teleportHero = (hero, location) => {
hero.location = location;
};
Export and import as a namespace
Alternatively omit "Hero" in function names to avoid renaming.
js
export { createHero as create,
moveHero as move,
teleportHero as teleport,
};
import * as Hero from "./hero.js";
const hero = Hero.create({ name: `Superjhemp` });
Hero.move(hero, [5, 20]);
Hero.teleport(hero, [100, 100]);
array, object, import, export
Use multiple lines for 4 or more items and consider it at 3 items.
array
.length = 0
to reset it.
[]
to create a new array.
.push()
to append.
const array = [
4,
5,
6,
];
Use map
, forEach
and other array methods to loop over an array.
Array.isArray
for type checking.
object
object[key] = value;
to add a key value pair.
delete object[key]
to remove a key value pair.
{}
to create a new object.
Use the shorthand when possible , computed properties
const b = 7;
const c = `key`;
const object = [
a: 5,
[c]: 9,
b,
];
Use Object.keys
, Object.values
, Object.entries
to convert it to an array, then use array methods to loop over an object.
typeof x === `object` && x !== null
for type checking.
Object.hasOwn
to check the existence of a property-value pair.
strings
Use backtick `
as they always work. '
and "
can be included directly. Concatenation is done via ${x}
. Adding line breaks just works. \n
can also be used. There is no need to switch the delimiter.
const s = `string`;
const s2 = `Tom says: "That's my ${s}"`;
typeof x === `string`
for type checking .
numbers
Prefer Number.isFinite
over isNaN
. Number.isFinite
can also be used for type checking. It returns false if the number is NaN
, Infinity
, -Infinity
or another type. typeof x === `number`
will return true if x
is NaN
, Infinity
, -Infinity
as well.
symbols
const mySymbol = Symbol();
const yourSymbol = Symbol(`A meaningful description`);
yourSymbol.description;
Do not use Symbol.for
because they are disguised globals. typeof mySymbol === `symbol`
for type checking.
date
Use numbers to store dates. Use the Date
built-in to display them.
Store the current time.
const time = Date.now();
To display the date.
const date = new Date();
date.setTime(time);
date.toLocaleString();
date.toLocaleTimeString();
date.toLocaleDateString();
// and other toString variants
sets
Yes
new Set();
When to use sets over arrays
- No duplicates are wanted
- The order is irrelevant
maps
Yes
new Map();
When to use maps over objects
In the following situations:
- Keys are something else than string or symbol
- Continuous adding and removal of key-value pairs
- Keys are unknown or can be anything
absence of value
Use undefined
, avoid null
. undefined
is the default that is already used by the language, for example: destructuring when missing, default return value, unassigned variable etc.
Optional chaining
Yes.
The chain stops when expecting a value from undefined. Know the difference between those:
(undefined)?.startsWith(`a`); // undefined
{}?.startsWith(`b${1+2}`); // TypeError and what is inside the parentheses is computed
{}?.startsWith?.(`c`); // undefined
{}.a?.b?.c?.["d"] // undefined
In the second example we expect a value from undefined and the chain immediately stops.
In the second example we expect a value from an object (startsWith), which resolves to undefined, and then we call it (cannot call undefined).
In the third example we also use an optional call (?. + parentheses) to only call it if it is callable.
Booleans
true
or false
, use Boolean
function to force cast to a Boolean. Do not use !!
to cast to a Boolean. Leverage truthy values in if
and while
.
promises
Prefer promises over callbacks for oneasync
, await
, Promise.all
.
if else
Always use multi-line brackets.
if (condition) {
}
if (condition) {
} else {
}
if (condition) {
} else if (otherCondition) {
} else {
}
import export
Put them at the top of the file as they will be executed first. Put exports before imports as they are executed before. Use named exports, as they allow for extension and are always live bindings. Do not inline named exports in the middle of the file to make it obvious what is exported by reading the first line.
export { y };
import FastAverageColor from "fast-average-color/dist/index.esm.js";
import { x } from "./x.js";
import * as z from "./z.js";
const y = 5;
import/export order
- exports with optional line break
- imports of standard library
- imports of runtime library (NodeJS, Deno, etc)
- imports of external dependencies
- imports of local files
- optional line break
- declaration of constants that are import related (some libraries only export default)
- optional two line breaks then begin of code
Limit variable reach
Historically it has been done with an immediately invoked function expression, now that let and const are available and block scoped use a simple block. With an iife:
// do not expose i
let nextSquare;
(function () {
let i = 0;
nextSquare = function () {
i += 1;
return i ** 2;
};
}());
With a block:
let nextSquare;
{
let i = 0;
nextSquare = function () {
i += 1;
return i ** 2;
};
}
global built-ins
Use directly.
setTimeout(() => {
}, 1000);
avoid
window.setTimeout(() => {
}, 1000);
2. Features to avoid
this
and class
Read Why disallow the class keyword
Avoid this
. And associated bind
, call
, apply
, class
, prototype
, super
, new
, extends
, Object.create
, Object.setPrototypeOf
, Object.getPrototypeOf
, __proto__
, instanceof
, typeof
, .prototype.isPrototypeOf
.
Any function can return an object, any function can take an object as first argument and operate on it. Every function can compose or combine results of other functions.
Prefer regular objects and functions over class
. These can be exported from the same file and one can still use Object-oriented patterns without the class
keyword.
bind
can still be useful for currying with undefined as first value.
apply
Prefer array spread syntax.
const numbers = [4, 5];
const max = Math.max.apply(undefined, numbers);
const numbers = [4, 5];
const max = Math.max(...numbers);
regular expressions
Avoid when possible. Regular expression is a powerful language that overlaps with JS constructs. Often a single line can obscure recursive and complex instructions. The tooling to debug and assess performance of regexs are lower than JS.
Avoid the constructor syntax. Use named groups.
let r = /[a-z]+/;
Alternatives
Use regular objects
Regex are sometimes used to extract information out of a big string. Consider using an object instead that can be extracted with JSON for example.
Raw String
String.prototype.includes
String.prototype.indexOf
String.prototype.replace
String.prototype.endsWith
String.prototype.startsWith
ternary operator
Prefer if else
, as they can be extended and are cleaner.
switch case break
Prefer if else
.
conditional assignment
Avoid
const x = y || z;
const r = z && u || w;
Prefer if else
.
chained method calls
Keep lines independent.
comma operator
The comma operator is well known to write one-liners and hurts readability. Put each instruction on individual lines.
for of, for in, for () loops
Prefer array built-ins to iterate.
Export objects with multiple variables
Prefer exporting variables individually via named exports. Three shake friendlier and less runtime overhead.
Avoid
export { constants };
const constants = {
X: 1,
Y: 2,
};
Prefer
export { X, Y };
const X = 1;
const Y = 2;
default exports
No clear way to add or subtract functionality. Prefer named exports.
Meta-programming and Proxy
Avoid whenever possible. Avoid the use of Proxy
, Object.defineProperty
, Function.name
and Function.length
.
getters and setters
For setters that have additional functionality prefer explicit functions. Otherwise use public members.
Implicit type conversion
Prefer explicit type conversion
const x = `1`;
// const y = +x;
const y = Number(x);
const y = 1;
//const x = "" + y;
const x = String(x);
arguments
Do not use the special arguments
, use rest arguments instead.
Soft equal
Avoid ==
and !=
, prefer ===
and !==
.
Automatic semicolon insertion
Avoid relying on asi to be consistent and have less edge cases to remember.
try catch over everything
Use try catch
on individual statements that are expected to fail for code clarity it makes it obvious which one is expected to fail. Generally avoid try catch
entirely if possible. Validate input as early as possible to avoid potential error management in the middle of the function body.
try catch finally
Avoid finally, in most cases, putting the statements after try catch has the same effect.
generators, iterators
Avoid generators and associated yield
keyword. Prefer functions that return a function with a closure.
side effect inside if
Avoid any side effect inside the condition of an if
. Same for while
and for
.
disable the linter on individual files, lines
Be consistent.
multi level destructuring
Avoid,
const { body: { className } } = document;
prefer 1 line per level.
const { body } = document;
const { className } = body;
unary operators ++ --
Avoid those. Prefer +=
and -=
.
There is no need to remember the difference between --a
and a--
.
The increment amount can be something else than 1. It can be any variable.
It is consistent with other operators such as /= , *= , **= , %=
etc
let a = 1;
a += 1;
a /= 2;
3. Features to never use
global variables
Prefer explicit exported variables. With the exception of polyfills.
Modifications of built-ins
Prefer exporting new variables with different names.
with
Use object destructuring instead.
non-strict mode
Strict mode throws error when doing things like assigning to undefined
and will prevent mistakes. Strict mode is always enabled when using import/export
.
new Boolean, new String, new Array, new Object
These have no real purpose other than introduce subtle bugs. Use Boolean
, String
and array literal []
instead. new Object()
is harmless but use the object literal shorthand {}
for consistency.
void operator
Don't use the void operator, use a dedicated minifier instead.
Details
naming
Use semantic long descriptive names for variables. Do not use names that are already used by builtspecial keywords.
Camel case for regular variables and file names. No spaces.
Optionally Pascal case for creator/constructors.
Optionally MACRO_CASE (all caps with underscores) for top level constants that do not change across versions and runs.
Package/module names and script names with Kebab case (lowercase with dashes). No dots.
Default imports use the same letters as the package name.
Examples
majo-ubjson
as package name
majoUbjson.js
entry file (not index.js)
bundle
, minify-html
as script names
// ALWAYS the same
const { PI } = Math;
const HALF_PI = PI / 2;
// regular variables that can be changed
const radius = 1;
const volumeSphere = (4 / 3) * PI * radius ** 3;
// constructor
const BookStore = class {
};
// creator Pacal (CreateHero) or camel case (createHero)
const createHero = function () {
const hero = {};
return hero;
};
// No !
// confusing
const await = 2;
// overshadows built-in
const Date = { year: 2019 };
indentation
Indent with 4 spaces, or 1 tab.
spacing
Start with prettier.
Extensions
If any non standard features are used, use should be documented. If any features are not in the standard track pipeline, the file extension should reflect that.
Prefer to avoid use of features that have not reached stage 4.
Empty if else bodies
Yes when it helps document the code, for example by acknowledging a given situation.
magic numbers
Use constants at the top of the scope.
package.json (when used)
Order by what you want to see the most.
- Name, version, description, license, author, homepage (What is it ?)
- type, main, module, browser, exports, bin (What is the entry file ?)
- scripts (What commands are available ?)
- dependencies, optionalDependencies, peerDependencies devDependencies (What does it use ?)
- configs, engines, os, cpu (How is it configured ?)
- files, repository, keywords, private, publishConfig (Meta, publishing, distribution)
Avoid peerDependencies because they create more problems than they solve, instead specify them in the documentation.
Minimal example
{
"name": "utilsac-example",
"version": "15.0.0",
"description": "Utility functions",
"license": "CC0-1.0",
"type": "module",
"main": "utility.js",
"repository": {
"type": "git",
"url": "git://github.com/GrosSacASac/utilsac-example.git"
}
}
About
Lint set-up
Install eslint and eslint-red rules
npm i -D eslint eslint-config-red
Inside package.json
"eslintConfig": {
"extends": ["red"],
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module",
"ecmaFeatures": {}
},
"env": {
"es2022": true,
"browser": true,
"node": true,
"serviceworker": true,
"worker": true
},
"rules": {}
}
Below es2021, remove unused environment.
If code is meant for other environments other than browser, add them to "env"
Inside package.json > scripts
"lint-fix": "eslint --ignore-path .gitignore --fix source",
"lint": "eslint --ignore-path .gitignore source",
where source is the folder containing all the files to lint.
Inspiration
Contributors
- GrosSacASacs
- fschoenfeldt
License
Public Domain