se-javascript-style-guide v1.0.3
Simple Energy Javascript Style Guide
How to use: Read through all of the sections below to understand, generally, our preferred implementation. The Implementation Standards section should be well understood committed to memory, so-to-speak. The Syntax Rules section should be read over thoroughly, but doesn't need to be memorized since the rules are enforced by our eslint configuration package.
Please keep in mind that there are many enforced syntax rules that are not outlined in this guide (we've only highlighted the most frequently violated rules here). If you are not clear about the meaning a lint error, or you are not sure how to repair the error, please refer to the eslint documentation to remediate.
I. Implementation Standards 1. Thorough Consideration 1. Naming Things 1. Consistent References 1. Immutability 1. Object Oriented & Functional Approaches 1. Testing
II. Syntax Rules 1. References 1. Objects 1. Arrays 1. Destructuring 1. Strings 1. Functions 1. Arrow Functions 1. Classes & Constructors 1. Modules 1. Iterators and Generators 1. Variables 1. Comparison Operators & Equality 1. Blocks 1. Control Statements 1. Comments 1. Commas 1. Semicolons 1. Type Casting & Coercion 1. Accessors 1. Events
I. IMPLEMENTATION STANDARDS
Thorough Consideration
1.1 In general, implementation should be an expression of thorough analysis of the problem to be solved, and careful evaluation of side-effects of various strategies. Rushing into an implementation strategy often leads to an increased level of effort and limited extendability. Don't start writing until you have planned your implementation from beginning to end - including a plan for managing deployment and maintaining backward compatibility.
Here are some questions you should be asking when considering solutions:
- What is the simplest solution?
- Is this solution open for extension?
- Have I considered a variety of possible solutions?
- Am I using the right tool for the right job?
- What effect will this implementation have on other areas of the system?
- Will others be able to easily understand what I am doing?
Naming Things
2.1 Do not be terse when naming things. The names you assign should clearly summarize the object being named. Consider the statement
const a = b + c;
. While it is a valid statement, it does not provide any context as to it's purpose. The same statement, rewritten asconst totalHarvest = barleyHarvest + alfalfaHarvest;
is much easier reason about because it's context is clear. Avoid short, ambiguous names; be descriptive.// bad const sh = 'corn'; // good const springHarvest = 'corn';
2.2 Use camelCase when naming objects, functions, and instances.
// bad const DillPickles = {}; const delicious_dill_pickle = {}; function EatPickle() {} // good const deliciousDillPickle = {}; function eatPickle() {}
2.3 Use PascalCase only when naming constructors or classes.
// bad function farmHand(options) { this.name = options.name; } const farmHand = new farmHand({ name: 'Old McDonald', }); // good class FarmHand { constructor(options) { this.name = options.name; } } const farmHand = new FarmHand({ name: 'Old McDonald', });
2.4 A base filename should exactly match the name of its default export. Omit
index.js
if importing an index from a directory.// file 1 contents in ./Crop/index.js class Crop { // ... } export default Crop; // file 2 contents export default function maxYield() { return 999; } // file 3 contents export default function totalAllCrops() {} // in some other file // bad import Crop from './Crop/index'; import maxYield from './MaxYield'; import totalAllCrops from './totalAllCrops'; // good import Crop from './Crop'; import maxYield from './maxYield'; import totalAllCrops from './totalAllCrops';
2.5 Use camelCase when you export-default a function. Your filename should be identical to your function’s name.
function feedTheCattle() { // ... } export default feedTheCattle;
2.6 Use PascalCase when you export a constructor / class / singleton / function library / bare object.
const TasksOnTheFarm = { irrigate: { }, }; export default TasksOnTheFarm;
2.7 Acronyms and initialisms should always be all capitalized, or all lowercased.
// bad import GmoFree from './GmoFree'; // good import GMOFree from './GMOFree'; // good import gmoFree from './gmoFree';
2.8 Use uppercase names (with underscores) for application wide constants, especially configuration variables.
// bad const LOCAL_VARIABLE = 'should not be uppercased'; // good export const API_KEY = 'SOMEKEY'; // good export const SOME_FARM = { FARM_REGION: 'Colorado', };
2.9 Name imported npm modules consistently with module name. Constructors are uppercase, functions start with lowercase
// bad import classNames from 'classnames'; import ClassNames from 'class-names'; import classnames from 'class-names'; // good import classnames from 'classnames'; // Function default export import classNames from 'class-names'; // Function default export // good import Classnames from 'classnames'; // Constructor default export import ClassNames from 'class-names'; // Constructor default export
Consistent References
3.1 Do not rename a parameter reference if it's value is not modified by the function. Renaming or mapping unmodified references makes it very difficult to track values through a call chain.
// bad function plantCrop(crop, farmer) { return { field: crop, worker: farmer, plantedOn: new Date(), } } function waterCrop({ field, worker, datePlanted }) { return { cropType: field, farmHand: worker, planted: datePlanted, wateredOn: new Date(), } } function harvestCrop({ cropType, farmHand, planted, wateredOn }) { const threeMonthsInMilliseconds = 7776000000; const threeMonthsAgo = new Date().getTime() - threeMonthsInMilliseconds; if (plantedOn < threeMonthsAgo) { return { harvested: harvestedCrop, farmer: farmHand, plantedDate: planted, wateredDate: wateredOn, harvestedOn: new Date() } } } const plantedCrop = plantCrop('Amy', 'Walnut'); const wateredCrop = waterCrop(plantedCrop); const harvestedCrop = harvestCrop(wateredCrop); // plantedCrop.field = 'Walnut' // wateredCrop.cropType = 'Walnut' // harvestedCrop.harvested = 'Walnut' // good function plantCrop(crop, farmer) { return { crop, farmer, plantedOn: new Date(), } } function waterCrop({ crop, farmer, plantedOn }) { return { crop, farmer, plantedOn, wateredOn: new Date(), } } function harvestCrop({ crop, farmer, plantedOn, wateredOn }) { const threeMonthsInMilliseconds = 7776000000; const threeMonthsAgo = new Date().getTime() - threeMonthsInMilliseconds; if (plantedOn > threeMonthsAgo) { return { crop, farmer, plantedOn, wateredOn, harvestedOn: new Date(), } } } const plantedCrop = plantCrop('Amy', 'Walnut'); const wateredCrop = waterCrop(plantedCrop); const harvestedCrop = harvestCrop(wateredCrop); // plantedCrop.crop = 'Walnut' // wateredCrop.crop = 'Walnut' // harvestedCrop.crop = 'Walnut'
3.2 When deriving a value from a given reference, assign a name to the derivative that contains some reference to the original name. This provides useful context for understanding the source of the new reference.
// bad function washFruit(fruitName) { const replaced = fruitName.replace(/(moldy|dirty)/, ''); return `Enjoy your delicious ${replaced}`; } // good function washFruit(fruitName) { const fruitNameCleaned = fruitName.replace(/(moldy|dirty)/, ''); return `Enjoy your delicious ${fruitNameCleaned}`; } // bad const result = veggies.filter(veggie => veggie.ripe); // good const veggiesFiltered = veggies.filter(veggie => veggie.ripe);
Immutability
To avoid unwanted mutations, don't work directly on a reference. Instead, create a copy of the object first or use a pure function that constructs a new reference with transformed values. The following patterns can be used to avoid unwanted mutations on object
s and array
s.
4.1 Shallow copy object: Use the spread operator to copy object references. This also provides a convenience for updating values on the new copy.
const harvest = { type: 'Hops', weight: '22kg', } const harvestAfterDamage = { ...crop, weight: '19kg', }
4.2 Deep copy object: Use a recursive function to clone all deeply nested object properties.
const harvest = { type: 'Hops', stats: { weight: '22kg', harvestedBy: 'John Farmhand' }, } function cloneObject(obj) { const clone = {}; for (let i in obj) { if (obj[i] !== null && typeof obj[i] === 'object') { clone[i] = cloneObject(obj[i]); } else { clone[i] = obj[i]; } } return clone; } const clonedHarvest = cloneObject(crop);
4.3 Copy array: Use the spread operator to copy array references. This also provides a convenience for adding values to the new copy.
const veggies = ['tomato', 'cucumber', 'potato']; const moreVeggies = [...veggies, 'celery'];
4.4 Shallow copy object array: Use a functional array method to return a new array of copied objects.
const veggies = [ { name: 'tomato', color: 'red' }, { name: 'potato', color: 'brown' } ]; const moreVeggies = veggies.map(veggie => { return { ...veggie }; });
Object Oriented & Functional Approaches
5.1 Object Oriented: When a feature you are writing relies on identity, state and methods, it is best to use an object oriented approach. Particularly, if the piece of functionality will be appropriated across a number of implementations and be extended over time, it is best to encapsulate the functionality using ES6 classes so that it is decoupled from the constraints of the application framework.
5.2 Functional: When a piece of functionality is required only for transforming data and has no need to maintain identity or state, a functional implementation is preferred, especially when collected into reusable modules.
5.3 Always: Even when using an OOP approach, you should endeavor to apply functional paradigms to the overall design; minimizing state changes and side-effects should always be a primary concern. When mutations are required, try to relegate them to one location so that they are easier to monitor and maintain.
Testing
- Whichever testing framework you use, you should be writing tests!
- 100% test coverage is a good goal to strive for, even if it’s not always practical to reach it.
- Strive to cover all branching logic; many bugs result from poorly implemented control flow.
- Whenever you fix a bug, write a regression test. A bug fixed without a regression test is almost certainly going to break again in the future.
II. SYNTAX RULES
References
1.1 Use
const
for all of your references; this will help ensure immutability of reference values. If you must reassign a reference, uselet
. Do not usevar
as it is function-scoped, rather than block-scoped.When you access a primitive type
string
,number
,boolean
,null
,undefined
, andsymbol
, you work directly on its value. When you access a complex type likeobject
,array
, andfunction
, you work on a reference to it's value.No symbols: Symbols cannot be faithfully polyfilled, so they should not be used when targeting browsers/environments that don’t support them natively.
Objects
2.1 Use property value shorthand, object method shorthand, and group your shorthand properties at the beginning of your object declaration.
const farmParam = 'some_param'; const farmName = 'Some Farm'; // bad const farm = { farmHands: [], hasFruit: true, hasVeggies: false, farmName: farmName, hasLivestock: true, farmParam: farmParam, addFarmHand: function (name) { return this.farmHands.push(name); } }; // good const farm = { farmName, farmParam, farmHands: [], hasFruit: true, hasVeggies: false, hasLivestock: true, addFarmHand(name) { return this.farmHands.push(name); }, };
Arrays
3.1 Use
push
instead of direct assignment to add items to an array.const farmersWardrobe = []; // bad farmersWardrobe[farmersWardrobe.length] = 'Overalls'; // good farmersWardrobe.push('Overalls');
3.2 To convert an iterable object to an array, use spreads
...
instead ofArray.from
.const fruitListing = document.querySelectorAll('.fruit__list'); // good const fruitNodes = Array.from(fruitListing); // best const fruitNodes = [...fruitListing];
3.3 Use
Array.from
for converting an array-like object to an array.const typesOfVeggies = { 0: 'squash', 1: 'carrot', 2: 'celery' }; // bad const veggies = Array.prototype.slice.call(typesOfVeggies); // good const veggies = Array.from(typesOfVeggies);
3.4 Use return statements in array method callbacks. It’s ok to omit the return if the function body consists of a single statement returning an expression without side effects.
// good ['squash', 'carrot', 'celery'].map((veggie) => { return `I love ${veggie}`; }); // good ['squash', 'carrot', 'celery'].map(veggie => `I love ${veggie}`);
Destructuring
4.1 Use object destructuring when accessing and using multiple properties of an object.
// bad function getFruitName(fruit) { const genus = fruit.genus; const species = fruit.species; return `${genus} ${species}`; } // good function getFruitName(fruit) { const { genus, species } = fruit; return `${genus} ${species}`; } // best function getFruitName({ genus, species }) { return `${genus} ${species}`; }
4.2 Use array destructuring.
const veggies = ['squash', 'carrot', 'celery']; // bad const firstVeggie = veggies[0]; const secondVeggie = veggies[1]; // good const [firstVeggie, secondVeggie] = veggies;
4.3 Use object destructuring for multiple return values, not array destructuring.
// bad function tillTheSoil(input) return [north, south, east, west]; } // the caller needs to think about the order of return data const [north, __, east] = tillTheSoil(input); // good function tillTheSoil(input) { return { north, south, east, west }; } // the caller selects only the data they need const { north, east } = tillTheSoil(input);
Strings
5.1 Use single quotes
''
for strings.5.2 Strings that cause the line to go over 100 characters should not be written across multiple lines using string concatenation.
// bad const errorMessage = 'Growing and harvesting crops requires many tasks. ' + 'Before the crop is planted, preparing the soil with disking, tilling ' + '(vertical or horizontal), and fertilizing is sometimes required.'; // good const errorMessage = 'Growing and harvesting crops requires many tasks. Before the crop is planted, preparing the soil with disking, tilling (vertical or horizontal), and fertilizing is sometimes required.';
5.3 When programmatically building up strings, use template strings instead of concatenation.
// bad function enjoyFruit(fruitName) { return 'Golly, I sure love ' + fruitName + '?'; } // good function enjoyFruit(fruitName) { return `Golly, I sure love ${fruitName}?`; }
Functions
6.1 Use named function expressions instead of function declarations.
// bad function pickFruit() { // ... } // bad const pickFruit = function () { // ... }; // good // lexical name distinguished from the variable-referenced invocation(s) const pickFruit = function pickFruitInTheSummer() { // ... };
6.2 Never use
arguments
, opt to use rest syntax...
instead.// bad function makeSmoothieFromFruits() { const fruits = Array.prototype.slice.call(arguments); return fruits.join(''); } // good function makeSmoothieFromFruits(...fruits) { return fruits.join(''); }
6.3 Use default parameter syntax rather than mutating function arguments.
// bad function makeDinner(ingredients) { ingredients = ingredients || {}; // ... } // good function makeDinner(ingredients = {}) { // ... }
6.4 Always put default parameters last.
// bad function makeDinner(ingredients = {}, participants) { // ... } // good function makeDinner(participants, ingredients = {}) { // ... }
6.6 Never mutate parameters.
// bad function fruit(type) { type.name = 'melon'; } // good function fruit(type) { const fruitName = Object.prototype.hasOwnProperty.call(type, 'name') ? type.name : 'melon'; }
6.7 Never reassign parameters.
// bad function fruit(name) { name = 'tomato'; } // good function fruit(name) { const fruitName = name || 'tomato'; // ... }
Arrow Functions
7.1 When you must use an anonymous function (as when passing an inline callback), use arrow function notation.
// bad ['squash', 'carrot', 'celery'].map(function (veggie) { return `I love ${veggie}`; }); // good ['squash', 'carrot', 'celery'].map((veggie) => { return `I love ${veggie}`; });
7.2 If the function body consists of a single statement returning an expression without side effects, omit the braces and use the implicit return. Otherwise, keep the braces and use a
return
statement.// bad ['squash', 'carrot', 'celery'].map(number => { return `I love ${veggie}`; }); // good ['squash', 'carrot', 'celery'].map(veggie => `I love ${veggie}`); // good let totalVeggies = 0; ['squash', 'carrot', 'celery'].map((veggie) => { totalVeggies += 1; return `I've eaten ${totalVeggies} veggies`; }); // good ['squash', 'carrot', 'celery'].map((veggie) => ({ name: veggie, }));
Classes & Constructors
8.1 Always use
class
. Avoid manipulatingprototype
directly.// bad function Harvest(fields = []) { this.queue = [...fields]; } Harvest.prototype.yieldCrop = function () { const crop = this.queue[0]; this.queue.splice(0, 1); return crop; }; // good class Harvest { constructor(fields = []) { this.queue = [...fields]; } yieldCrop() { const crop = this.queue[0]; this.queue.splice(0, 1); return crop; } }
8.2 Use
extends
for inheritance.// good class WalnutOrchard extends Orchard { // ... }
8.3 Methods can return
this
to help with method chaining.// good class Field { water() { this.watering = true; return this; } fertilize(height) { this.fertilize = true; return this; } } const field = new Field(); field.water() .fertilize();
Modules
9.1 Always use modules (
import
/export
) over a non-standard module system. You can always transpile to your preferred module system.// bad const HeavyEquipment = require('./HeavyEquipment'); module.exports = HeavyEquipment.tractor; // best import { tractor } from './HeavyEquipment'; export default tractor;
9.2 In modules with a single export, prefer default export over named export.
// bad export function waterOrchard() {} // good export default function waterOrchard() {}
Iterators and Generators
10.1 Don’t use iterators. Prefer JavaScript’s higher-order functions instead of loops like
for-in
orfor-of
.Use
map()
/every()
/filter()
/find()
/findIndex()
/reduce()
/some()
/ ... to iterate over arrays, andObject.keys()
/Object.values()
/Object.entries()
to produce arrays so you can iterate over objects.const numbers = [1, 2, 3, 4, 5]; // bad let allVeggies = 0; for (let number of numbers) { allVeggies += number; } // good let allVeggies = 0; numbers.forEach((number) => { allVeggies += number; }); // best const allVeggies = numbers.reduce((total, number) => total += number, 0);
Properties
11.1 Use dot notation when accessing properties.
const oldMcDonald = { farm: true, age: 88, }; // bad const hasFarm = oldMcDonald['farm']; // good const hasFarm = oldMcDonald.farm;
11.2 Use bracket notation
[]
when accessing properties with a variable.const oldMcDonald = { farm: true, age: 88, }; function getProp(prop) { return oldMcDonald[prop]; } const hasFarm = getProp('farm');
Variables
12.1 Group all your
const
s and then group all yourlet
s.// bad let i; const fruits = getFruits(); let bestFruit; const isFarmer = true; let totalFruits; // good const isFarmer = true; const fruits = getFruits(); let bestFruit; let i; let totalFruits;
12.2 Assign variables where you need them, but place them in a reasonable location.
// bad function checkFruitName(fruitName) { const name = getName(); if (fruitName === 'test') { return false; } if (name === 'test') { this.setName(''); return false; } return name; } // good function checkFruitName(fruitName) { if (fruitName === 'test') { return false; } const name = getName(); if (name === 'test') { this.setName(''); return false; } return name; }
12.3 Avoid using unary increments and decrements (
++
,--
) as they are subject to automatic semicolon insertion and can cause silent errors with incrementing or decrementing values within an application.// bad let totalHarvest = 1; totalHarvest++; --totalHarvest; // good let totalHarvest = 1; totalHarvest += 1; totalHarvest -= 1;
Comparison Operators & Equality
12.1 Conditional statements such as the
if
statement evaluate their expression using coercion with theToBoolean
abstract method and always follow these simple rules:- Objects evaluate to true
- Undefined evaluates to false
- Null evaluates to false
- Booleans evaluate to the value of the boolean
- Numbers evaluate to false if +0, -0, or NaN, otherwise true
Strings evaluate to false if an empty string
''
, otherwise trueif ([0] && []) { // true // an array (even an empty one) is an object, objects will evaluate to true }
12.2 Use shortcuts for booleans, but explicit comparisons for strings and numbers.
// bad if (isVeggie === true) { // ... } // good if (isVeggie) { // ... } // bad if (fruitName) { // ... } // good if (fruitName !== '') { // ... } // bad if (cropsHarvested.length) { // ... } // good if (cropsHarvested.length > 0) { // ... }
12.3 Use braces to create blocks in
case
anddefault
clauses that contain lexical declarations (e.g.let
,const
,function
, andclass
).// bad switch (crop) { case 'walnut': let price = 1; break; case 'squash': const price = 2; break; case 'carrot': function countCarrots() { // ... } break; default: class Fruit {} } // good switch (crop) { case 'walnut': { let price = 1; break; } case 'squash': { const price = 2; break; } case 'carrot': { function countCarrots() { // ... } break; } default: { class Fruit {} } }
12.4 Ternaries should not be nested and generally be single line expressions.
// good const typeOfHarvest = name === 'tomato' ? 'fruit' : 'veggie'; // best const typeOfHarvest = name === 'tomato' ? 'fruit' : 'veggie';
Blocks
13.1 Use braces with all multi-line blocks.
// bad if (farmer) return false; // good if (farmer) { return false; }
13.2 If you’re using multi-line blocks with
if
andelse
, putelse
on the same line as yourif
block’s closing brace.// bad if (farmer) { rideTractor(); } else { rideBus(); } // good if (test) { rideTractor(); } else { rideBus(); }
13.3 If an
if
block always executes areturn
statement, the subsequentelse
block is unnecessary. Areturn
in anelse if
block following anif
block that contains areturn
can be separated into multipleif
blocks.// bad function checkType(crop) { if (crop.isFruit) { return 'Fruit'; } else { return 'Veggie'; } } // good function checkType(crop) { if (crop.isFruit) { return 'Fruit'; } return 'Veggie'; } // bad function takeMeal() { if (lunch) { return 'Get yer lunch'; } else if (dinner) { return 'Get yer dinner'; } } // good function takeMeal() { if (lunch) { return 'Get yer lunch'; } if (dinner) { return 'Get yer dinner'; } }
Control Statements
14.1 In case your control statement (
if
,while
etc.) gets too long or exceeds the maximum line length, each (grouped) condition could be put into a new line. The logical operator should begin the line.// bad if ((totalVeggies === 999 || period === 'evening') && theTractorNeedsRepair() && theFarmerNeedsSomeHelp()) { retireForTheEvening(); } // good if ( (totalVeggies === 999 || period === 'evening') && theTractorNeedsRepair() && theFarmerNeedsSomeHelp() ) { retireForTheEvening(); }
14.2 Don't use selection operators in place of control statements.
// bad !isEvening && waterThemFields(); // good if (!isEvening) { waterThemFields(); }
Comments
15.1 Prefixing your comments with
FIXME
orTODO
helps other developers quickly understand if you’re pointing out a problem that needs to be revisited, or if you’re suggesting a solution to the problem that needs to be implemented. These are different than regular comments because they are actionable. The actions areFIXME: -- need to figure this out
orTODO: -- need to implement
.15.2 Use
// FIXME:
to annotate problems.class WalnutOrchard extends Orchard { constructor() { super(); // FIXME: shouldn’t use a global here totalYield = 0; } }
15.3 Use
// TODO:
to annotate solutions to problems.class WalnutOrchard extends Orchard { constructor() { super(); // TODO: total should be configurable by an options param this.totalYield = 0; } }
Commas
16.1 Use trailing commas for multiline
arrays
,objects
,imports
,exports
andfunctions
.// bad const veggies = [ 'carrots', 'celery', 'tomato' ]; // good const veggies = [ 'carrots', 'celery', 'tomato', ]; // bad const farmer = { firstName: 'Mark', lastName: 'McDonald', birthYear: 1957 }; // good const farmer = { firstName: 'Mark', lastName: 'McDonald', birthYear: 1957, };
Semicolons
17.1 Use semicolons to terminate all statements because ASI is not reliable.
// bad - returns `undefined` function wakeBeforeDawn() { return 'Early bird gets the worm' } // good function wakeBeforeDawn() { return 'Early bird gets the worm'; }
Type Casting & Coercion
18.1 Strings
// bad const totalYield = this.cropYield.toString(); // isn’t guaranteed to return a string // bad const totalYield = new String(this.cropYield); // returns an object // good const totalYield = String(this.cropYield);
18.2 Numbers: Use
Number
for type casting andparseInt
always with a radix for parsing strings.const totalAcres = '4'; // bad const farmSize = new Number(totalAcres); // good const farmSize = Number(totalAcres); // bad const farmSize = parseInt(totalAcres); // good const farmSize = parseInt(totalAcres, 10);
18.3 Boolean
const totalYield = 0; // bad const hasCrop = new Boolean(totalYield); // good const hasCrop = Boolean(totalYield); // best const hasCrop = !!totalYield;
Accessors
19.1 Do not use JavaScript getters/setters as they cause unexpected side effects and are harder to test, maintain, and reason about. Instead, if you do make accessor functions, use
getVal()
andsetVal('hello')
.// bad class Orchard { get type() { // ... } set type(value) { // ... } } // good class Orchard { getType() { // ... } setType(value) { // ... } }
4 years ago
6 years ago