Coveo TypeScript (and JavaScript) Cloud Platform Standards
Other Standards
The current repository aggregates all code standards that must be respected when writing and reviewing TypeScript code related to the Coveo Cloud Platform. This document should therefore be read and applied by anyone having to write TypeScript code for the Coveo Cloud Platform.
The current repository is greatly inspired from Airbnb's JavaScript Style Guide, but most code examples were rewritten in TypeScript.
If you are somewhat new to JavaScript with ES6, we recommend you read the full Airbnb's JavaScript Style Guide. Feel free to use additional resources, there are tons of it out there.
If you are somewhat new to TypeScript, we recommend you follow this 5 minutes tutorial, and read the official TypeScript Handbook in full.
Adopting code standards means ranking the importance of code clarity and team conventions higher than personal style. In a more pragmatical perspective, we also believe that code standards have very positive and tangible effects on a team workflow:
- Team members get productive time back by avoiding subjective code style debates. If the code doesn't respect team conventions, simply point the author to the commonly accepted code standard that must be respected.
- Code reviews' focus are redirected towards what is most critical:
- Code architecture (Is there a better/more intelligent way to handle the use case at hand?)
- Code fidelity (Will the code crash in real life situations? Are all possible cases handled?)
- Code quality (Is the code well tested, meaningful, and DRY?)
- The code base gets easier to read and navigate for team members.
- Written standards (as opposed to implicit, word-to-mouth standards) allow newcomers to get up to speed faster by knowing how to write proper code from day one.
In summary, code standards make developers happier. Embrace them.
- Types
- References
- Objects
- Arrays
- Destructuring
- Strings
- Functions
- Arrow Functions
- Classes & Constructors
- Modules
- Iterators and Generators
- Properties
- Variables
- Hoisting
- Comparison Operators & Equality
- Blocks
- Control Statements
- Comments
- Whitespace
- Commas
- Semicolons
- Type Casting & Coercion
- Naming Conventions
- Accessors
- Events
- jQuery
- ECMAScript 6+ (ES 2015+) Styles
- Testing
- Reviewing
- Being Reviewed
- TypeScript
- Libraries and Frameworks
- Notes on Legacy Code
- Remaining Sections
Since this part appeared to have a more educational purpose, you can refer to the original Airbnb's JavaScript Style Guide for more information.
-
1.1 Use
const
for all of your references; avoid usingvar
.Why? This ensures that you can't reassign your references, which can lead to bugs and difficult to comprehend code.
// bad var a = 1; var b = 2; // good const a = 1; const b = 2;
-
1.2 If you must reassign references, use
let
instead ofvar
.Why?
let
is block-scoped rather than function-scoped likevar
.// bad var count = 1; if (true) { count += 1; } // good, use the let. let count = 1; if (true) { count += 1; }
-
1.3 Note that both
let
andconst
are block-scoped.// const and let only exist in the blocks they are defined in. { let a = 1; const b = 1; } console.log(a); // ReferenceError console.log(b); // ReferenceError
-
2.1 Use the literal syntax for object creation.
// bad const item: Interface = new Object(); // good const item: Interface = { value: 1 };
-
2.2 Methods defined on objects should use arrow functions.
// bad const atom: Interface = { value: 1, addValue: function (value: number): number { return atom.value + value; }, }; // good const atom: Interface = { value: 1, addValue: (value: number): number => value + 1, };
-
2.3 Use property value shorthand.
Why? It is shorter to write and descriptive.
const lukeSkywalker = 'Luke Skywalker'; // bad const obj: Interface = { lukeSkywalker: lukeSkywalker, }; // good const obj: Interface = { lukeSkywalker, };
-
2.4 Group your shorthand properties at the beginning of your object declaration.
Why? It's easier to tell which properties are using the shorthand.
const anakinSkywalker = 'Anakin Skywalker'; const lukeSkywalker = 'Luke Skywalker'; // bad const obj: Interface = { episodeOne: 1, twoJediWalkIntoACantina: 2, lukeSkywalker, episodeThree: 3, mayTheFourth: 4, anakinSkywalker, }; // good const obj: Interface = { lukeSkywalker, anakinSkywalker, episodeOne: 1, twoJediWalkIntoACantina: 2, episodeThree: 3, mayTheFourth: 4, };
-
2.5 Only quote properties that are invalid identifiers.
Why? In general we consider it subjectively easier to read. It improves syntax highlighting, and is also more easily optimized by many JS engines.
// bad const bad: Interface = { 'foo': 3, 'bar': 4, 'data-blah': 5, }; // good const good: Interface = { foo: 3, bar: 4, 'data-blah': 5, };
-
2.6 Use Underscore's
extend
andomit
functions to shallow-copy objects, and make sure not to mutate the original object...// very bad const original: Interface = { a: 1, b: 2 }; const copy: Interface = _.extend(original, { c: 3 }); // this mutates `original` ಠ_ಠ delete copy.a; // so does this // good const original: Interface = { a: 1, b: 2 }; const copy: Interface = _.extend({}, original, { c: 3 }); // copy => { a: 1, b: 2, c: 3 } // good const original: Interface = { a: 1, b: 2 }; const copy: Interface = _.omit(original, 'a'); // copy => { b: 2 }, _.omit does not mutate `original`
-
3.1 Use the literal syntax for array creation.
// bad const items: Interface[] = new Array(); // good const items: Interface[] = [];
-
3.2 Use return statements in array method callbacks. It's ok to omit the return if the function body consists of a single statement. We also encourage the use of the ternary operator in simple if/else cases.
// bad [1, 2, 3].map((x: number): number => { return x + 1; }); // good [1, 2, 3].map((x: number): number => x + 1); // good [1, 2, 3].map((x: number): number => { const y = x + 1; return x * y; }); // good inbox.filter((msg: string): boolean => { if (msg.subject === 'Mockingbird') { return msg.author === 'Harper Lee'; } else if (msg.subject === 'AnotherSubject') { return msg.author === 'The Author'; } return false; }); /* Simple if/else cases */ // bad inbox.filter((msg: string): boolean => { if (msg.subject === 'Mockingbird') { return msg.author === 'Harper Lee'; } else { return false; } }); // best inbox.filter((msg: string): boolean => msg.subject === 'Mockingbird' ? msg.author === 'Harper Lee' : false );
- 3.3 Use line breaks after open and before close array brackets if an array has multiple lines
// bad
const arr: number[][] = [
[0, 1], [2, 3], [4, 5],
];
const objectInArray: Interface[] = [{
id: 1,
}, {
id: 2,
}];
const numberInArray: number[] = [
1, 2,
];
// good
const arr: number[][] = [[0, 1], [2, 3], [4, 5]];
const objectInArray: Interface[] = [
{
id: 1,
},
{
id: 2,
},
];
const numberInArray: number[] = [
1,
2,
];
-
4.1 Use object destructuring when accessing and using multiple properties of an object.
Why? Destructuring saves you from creating temporary references for those properties.
// bad const getFullName = (user: User): string => { const firstName = user.personalInformation.firstName; const lastName = user.personalInformation.lastName; return `${firstName} ${lastName}`; } // good const getFullName = (user: User): string => { const { firstName, lastName } = user.personalInformation; return `${firstName} ${lastName}`; }
- 4.2 Use array destructuring.
const arr: number[] = [1, 2, 3, 4]; // bad const first: number = arr[0]; const second: number = arr[1]; // good const [first, second] = arr;
-
4.3 Use object destructuring for multiple return values, not array destructuring.
Why? You can add new properties over time or change the order of things without breaking call sites.
// bad const processInput = (input: Input): ProcessedInput => { // then a miracle occurs return [left, right, top, bottom]; } // the caller needs to think about the order of return data const [left, __, top] = processInput(input); // good const processInput = (input: Input): ProcessedInput => { // then a miracle occurs return { left, right, top, bottom }; } // the caller selects only the data they need const { left, top } = processInput(input);
-
5.1 Use single quotes
''
for strings.// bad const name: string = "Capt. Janeway"; // bad - template literals should contain interpolation or newlines const name: string = `Capt. Janeway`; // good const name: string = 'Capt. Janeway';
-
5.2 Strings that cause the line to go over 140 characters should not be written across multiple lines using string concatenation.
Why? Broken strings are painful to work with and make code less searchable.
// bad const errorMessage: string = 'This is a super long error that was thrown because \ of Batman. When you stop to think about how Batman had anything to do \ with this, you would get nowhere \ fast.'; // bad const errorMessage: string = 'This is a super long error that was thrown because ' + 'of Batman. When you stop to think about how Batman had anything to do ' + 'with this, you would get nowhere fast.'; // good const errorMessage: string = 'This is a super long error that was thrown because of Batman. When you stop to think about how Batman had anything to do with this, you would get nowhere fast.';
-
5.3 When programmatically building up strings, use template strings instead of concatenation.
Why? Template strings give you a readable, concise syntax with proper newlines and string interpolation features.
// bad const sayHi = (name: string): string => 'How are you, ' + name + '?'; // bad const sayHi = (name: string): string => ['How are you, ', name, '?'].join(); // bad const sayHi = (name: string): string => `How are you, ${ name }?`; // good const sayHi = (name: string): string => `How are you, ${name}?`;
-
5.4 Do not unnecessarily escape characters in strings.
Why? Backslashes harm readability, thus they should only be present when necessary.
// bad const foo: string = '\'this\' \i\s \"quoted\"'; // good const foo: string = `'this' is "quoted"`; const foo: string = `my name is '${name}'`;
-
5.5 Never hardcode a string that will appear in the UI in the code base. Localize the string in a dedicated json file.
Why? Coveo develops international products, strings appearing in the UI can be translated in multiple languages, thus we localize them.
-
6.1 Use named function expressions instead of function declarations. Prefer arrow functions if you do not absolutely need
function
to retrieve the properthis
context. Most importantly, define functions as a method inside a class whenever possible.Why? Function declarations are hoisted, which means that it’s easy - too easy - to reference the function before it is defined in the file. This harms readability and maintainability. If you find that a function’s definition is large or complex enough that it is interfering with understanding the rest of the file, then perhaps it’s time to extract it to its own module!
// bad const foo = function (bar: Interface): ReturnedInterface { // ... }; // bad const foo = function bar(bar: Interface): ReturnedInterface { // ... }; // good const foo = (bar: Interface): ReturnedInterface => { // ... }; // best (declare functions as methods inside classes) class MyClass { foo(bar: Interface): ReturnedInterface { // ... } }
-
6.2 Never name a parameter
arguments
. This will take precedence over thearguments
object that is given to every function scope.// bad const foo = (name: string, options: Options, arguments: Arguments): ReturnedInterface => { // ... }; // good const foo = (name: string, options: Options, args: Arguments): ReturnedInterface => { // ... };
-
6.3 Use default parameter syntax rather than mutating function arguments.
// really bad const handleThings = (options?: Options): ReturnedInterface { // No! We shouldn't mutate function arguments. // Double bad: if opts is falsy it'll be set to an object which may // be what you want but it can introduce subtle bugs. options = options || {}; // ... }; // still bad const handleThings = (options?: Options): ReturnedInterface => { if (options === void 0) { options = {}; } // ... }; // good const handleThings = (options: Options = {}): ReturnedInterface => { // ... };
-
6.4 Never reassign parameters.
Why? Reassigning parameters can lead to unexpected behavior, especially when accessing the
arguments
object. It can also cause optimization issues, especially in V8.// bad const f1 = (a: number) => { a = 1; // ... }; const f2 = (a?: number) => { if (!a) { a = 1; } // ... }; // good const f3 = (a?: number) => { const b: number = a || 1; // ... }; const f4 = (a: number = 1) => { // ... };
-
6.5 Prefer the use of the spread operator
...
to call variadic functions.Why? It's cleaner, you don't need to supply a context, and you can not easily compose
new
withapply
.// bad const x: number[] = [1, 2, 3, 4, 5]; console.log.apply(console, x); // good const x: number[] = [1, 2, 3, 4, 5]; console.log(...x); // bad new (Function.prototype.bind.apply(Date, [null, 2016, 8, 5])); // good new Date(...[2016, 8, 5]);
-
6.6 Functions with multiline signatures, or invocations, should be indented just like every other multiline list in this guide: with each item on a line by itself, with a trailing comma on the last item.
// bad const foo = (bar: string, baz: number, quux: number) => { // ... } // good const foo = ( bar: string, baz: number, quux: number, ) => { // ... } // bad console.log(foo, bar, baz); // good console.log( foo, bar, baz, );
-
7.1 When you must use function expressions (as when passing an anonymous function), use arrow function notation.
Why? It creates a version of the function that executes in the context of
this
, which is usually what you want, and is a more concise syntax. Only usefunction
if really needed.Why not? If you have a fairly complicated function, you might move that logic out into its own function declaration.
// bad [1, 2, 3].map(function (x: number): number { const y: number = x + 1; return x * y; }); // good [1, 2, 3].map((x: number): number => { const y: number = x + 1; return x * y; });
-
7.2 If the function body consists of a single expression, omit the braces and use the implicit return. Otherwise, keep the braces and use a
return
statement.Why? Syntactic sugar. It reads well when multiple functions are chained together.
// bad [1, 2, 3].map((x: number): string => { const nextValue: number = x + 1; `A string containing the ${nextValue}.`; }); // good [1, 2, 3].map((x: number): string => `A string containing the ${x}.`); // good [1, 2, 3].map((x: number): string => { const nextValue: number = x + 1; return `A string containing the ${nextValue}.`; }); // good [1, 2, 3].map((x: number, index: number): {[key: number]: number} => ({ [index]: x, }));
-
7.3 In case the expression spans over multiple lines, wrap it in parentheses for better readability.
Why? It shows clearly where the function starts and ends.
// bad ['get', 'post', 'put'].map((httpMethod: string): ReturnedInterface => Object.prototype.hasOwnProperty.call( httpMagicObjectWithAVeryLongName, httpMethod, ) ); // good ['get', 'post', 'put'].map((httpMethod: string): boolean => ( Object.prototype.hasOwnProperty.call( httpMagicObjectWithAVeryLongName, httpMethod, ) ));
-
7.4 Always include parentheses around arguments for clarity and consistency.
Why? Less visual clutter. Scopes the parameter with its type.
// bad (not even possible in TypeScript with typed parameters) [1, 2, 3].map(x: number => ( `A long string with the ${x}. It’s so long that we don’t want it to take up space on the .map line!` )); // good [1, 2, 3].map((x: number): string => ( `A long string with the ${x}. It’s so long that we don’t want it to take up space on the .map line!` ));
-
7.5 Avoid confusing arrow function syntax (
=>
) with comparison operators (<=
,>=
).// bad const itemHeight = (item: Item): string => item.height > 256 ? item.largeSize : item.smallSize; // good const itemHeight = (item: Item): string => (item.height > 256 ? item.largeSize : item.smallSize); // good const itemHeight = (item: Item): string => { const { height, largeSize, smallSize } = item; return height > 256 ? largeSize : smallSize; };
-
8.1 Always use
class
. Avoid manipulatingprototype
directly.Why?
class
syntax is more concise and easier to reason about.// bad function Queue(contents: any[] = []) { this.queue = [...contents]; } Queue.prototype.pop = function () { const value: any = this.queue[0]; this.queue.splice(0, 1); return value; }; // good class Queue { constructor(contents: any[] = []) { this.queue = [...contents]; } pop() { const value: any = this.queue[0]; this.queue.splice(0, 1); return value; } }
-
8.2 Use
extends
for inheritance.Why? It is a built-in way to inherit prototype functionality without breaking
instanceof
.// bad const inherits = require('inherits'); function PeekableQueue(contents) { Queue.apply(this, contents); } inherits(PeekableQueue, Queue); PeekableQueue.prototype.peek = () => this.queue[0]; // good class PeekableQueue extends Queue { peek() { return this.queue[0]; } }
-
8.3 Methods can return
this
to help with method chaining.// bad Jedi.prototype.jump = function (): boolean { this.jumping = true; return true; }; Jedi.prototype.setHeight = function (height: number) { this.height = height; }; const luke: Jedi = new Jedi(); luke.jump(); // => true luke.setHeight(20); // => undefined // good class Jedi { private jumping: boolean; private height: number; jump(): Jedi { this.jumping = true; return this; } setHeight(height: number): Jedi { this.height = height; return this; } } const luke: Jedi = new Jedi(); luke .jump() .setHeight(20);
-
8.4 It's okay to write a custom toString() method, just make sure it works successfully and causes no side effects.
class Jedi { private name: string; constructor(options: Options = {}) { this.name = options.name || 'no name'; } getName(): string { return this.name; } toString(): string { return `Jedi - ${this.getName()}`; } }
-
8.5 Classes have a default constructor if one is not specified. An empty constructor function or one that just delegates to a parent class is unnecessary.
// bad class Jedi { constructor() {} // ... } // bad class Rey extends Jedi { constructor(options: Options) { super(options); } // ... } // good class Rey extends Jedi { private name: string; constructor(options: Options) { super(options: Options); this.name = 'Rey'; } }
-
9.1 Always use modules (
import
/export
) over a non-standard module system.Why? Modules are the future, let's start using the future now.
// bad const AirbnbStyleGuide = require('./AirbnbStyleGuide'); module.exports = AirbnbStyleGuide.es6; // best import { es6 } from './AirbnbStyleGuide'; export es6;
-
9.2 Do not use wildcard (unless you're forced to) or default imports/exports.
Why? This makes sure you have a single default export.
// bad import * as AirbnbStyleGuide from './AirbnbStyleGuide'; // bad export default AirbnbStyleGuide; // inside one file import AirbnbStyleGuide from './AirbnbStyleGuide'; inside another file // good export AirbnbStyleGuide; // inside one file import { AirbnbStyleGuide } from './AirbnbStyleGuide'; // inside another file
-
9.3 And do not export directly from an import.
Why? Although the one-liner is concise, having one clear way to import and one clear way to export makes things consistent.
// bad // filename es6.ts export { es6 } from './AirbnbStyleGuide'; // good // filename es6.ts import { es6 } from './AirbnbStyleGuide'; export es6;
-
9.4 Only import from a path in one place, and each import should be on its own line.
Why? Having multiple lines that import from the same path can make code harder to maintain.
// bad import { named1 } from 'foo'; // … some other imports … // import { named2 } from 'foo'; // bad import { named1, named2 } from 'foo'; // import foo, { named1, named2, } from 'foo';
-
9.5 Do not export mutable bindings.
Why? Mutation should be avoided in general, but in particular when exporting mutable bindings. While this technique may be needed for some special cases, in general, only constant references should be exported.
// bad export let foo: number = 3; // good export const foo: number = 3;
-
9.6 Put all
import
s above non-import statements.Why? Since
import
s are hoisted, keeping them all at the top prevents surprising behavior.// bad import { foo } from 'foo'; foo.init(); import { bar } from 'bar'; // good import { foo } from 'foo'; import { bar } from 'bar'; foo.init();
-
9.7 Multiline imports should be indented just like multiline array and object literals.
Why? The curly braces follow the same indentation rules as every other curly brace block in the style guide, as do the trailing commas.
// bad import {longNameA, longNameB, longNameC, longNameD, longNameE} from 'path'; // good import { longNameA, longNameB, longNameC, longNameD, longNameE, } from 'path';
-
10.1 Don't use iterators. Prefer JavaScript's higher-order functions instead of loops like
for-in
orfor-of
.Why? This enforces our immutable rule. Dealing with pure functions that return values is easier to reason about than side effects.
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: number[] = [1, 2, 3, 4, 5]; // bad let sum: number = 0; for (let num: number of numbers) { sum += num; } sum === 15; // best (use the functional force) const sum: number = numbers.reduce((total: number, num: number): number => total + num, 0); sum === 15; // bad const increasedByOne: number[] = []; for (let i: number = 0; i < numbers.length; i++) { increasedByOne.push(numbers[i] + 1); } // best (keeping it functional) const increasedByOne: number[] = numbers.map((num: number): number => num + 1);
-
10.2 Don't use generators for now.
Why? They don't transpile well to ES5.
-
11.1 Use dot notation when accessing properties.
const luke: Interface = { jedi: true, age: 28, }; // bad const isJedi: boolean = luke['jedi']; // good const isJedi: boolean = luke.jedi;
-
11.2 Use bracket notation
[]
when accessing properties with a variable.const luke: Interface = { jedi: true, age: 28, }; const getProp = (prop: string) => luke[prop]; const isJedi: boolean = getProp('jedi');
-
12.1 Always use
const
orlet
(notvar
) to declare variables. Not doing so will result in global variables. We want to avoid polluting the global namespace. Captain Planet warned us of that.// bad superPower: SuperPower = new SuperPower(); // good const superPower: SuperPower = new SuperPower();
-
12.2 Use one
const
orlet
declaration per variable.Why? It's easier to add new variable declarations this way, and you never have to worry about swapping out a
;
for a,
or introducing punctuation-only diffs. You can also step through each declaration with the debugger, instead of jumping through all of them at once.// bad const items: Item[] = getItems(), goSportsTeam: boolean = true, dragonball: string = 'z'; // bad // (compare to above, and try to spot the mistake) const items: Item[] = getItems(), goSportsTeam: boolean = true; dragonball: string = 'z'; // good const items: Item[] = getItems(); const goSportsTeam: boolean = true; const dragonball: string = 'z';
-
12.3 Group all your
const
s and then group all yourlet
s.Why? This is helpful when later on you might need to assign a variable depending on one of the previous assigned variables.
// bad const items: Item[] = getItems(); let dragonball: string; const goSportsTeam: boolean = true; let len: number; // good const goSportsTeam = true; const items: Item[] = getItems(); let dragonball: string; let len: number;
-
12.4 Assign variables where you need them, but place them in a reasonable place.
Why?
let
andconst
are block scoped and not function scoped.// bad - unnecessary function call const checkName = (hasName: string): string|boolean => { const name: string = getName(); if (hasName === 'test') { return false; } if (name === 'test') { this.setName(''); return false; } return name; } // good const checkName = (hasName: string): string|boolean => { if (hasName === 'test') { return false; } const name: string = getName(); if (name === 'test') { this.setName(''); return false; } return name; }
-
12.5 Avoid using unary increments and decrements (++, --).
Why? Disallowing unary increment and decrement statements prevents you from pre-incrementing/pre-decrementing values unintentionally which can also cause unexpected behavior in your programs.
// bad const array: number[] = [1, 2, 3]; let num: number = 1; num++; --num; let sum: number = 0; let truthyCount: number = 0; for (let i: number = 0; i < array.length; i++) { let value: number = array[i]; sum += value; if (value) { truthyCount++; } } // good const array: number[] = [1, 2, 3]; let num: number = 1; num += 1; num -= 1; const sum: number = array.reduce((a: number, b: number): number => a + b, 0); const truthyCount: number = array.filter(Boolean).length;
Since this part appears to be more educational than anything else, you can refer to the original Airbnb Style Guide for more information about hoisting. In short, use const
and let
, and always avoid using var
.
- 13.1 Use
===
and!==
over==
and!=
.
-
13.2 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 true
if ([0] && []) { // true // an array (even an empty one) is an object, objects will evaluate to true }
-
13.3 Use shortcuts for conditionals as often as possible.
// bad if (isValid === true) { // ... } // good if (isValid) { // ... } // bad (unless you are testing for empty string only and not all falsy values) if (name !== '') { // ... } // good if (name) { // ... } // bad if (collection.length > 0) { // ... } // good if (collection.length) { // ... }
- 13.4 For more information see Truth Equality and JavaScript by Angus Croll.
-
13.5 Use braces to create blocks in
case
anddefault
clauses that contain lexical declarations (e.g.let
,const
,function
, andclass
).Why? Lexical declarations are visible in the entire
switch
block but only get initialized when assigned, which only happens when itscase
is reached. This causes problems when multiplecase
clauses attempt to define the same thing.// bad switch (foo) { case 1: let x: number = 1; break; case 2: const y: number = 2; break; case 3: const f = () => { // ... }; break; default: class C {} } // good switch (foo) { case 1: { let x: number = 1; break; } case 2: { const y: number = 2; break; } case 3: { const f = () => { // ... } break; } case 4: bar(); break; default: { class C {} } }
-
13.6 Ternaries should not be nested and generally be single line expressions.
// bad const foo: string|null = maybe1 > maybe2 ? "bar" : value1 > value2 ? "baz" : null; // good const maybeNull: string|null = value1 > value2 ? 'baz' : null; const foo: string|null = maybe1 > maybe2 ? 'bar' : maybeNull;
-
13.7 Avoid unneeded ternary statements.
// bad const foo: Interface = a ? a : b; const bar: boolean = c ? true : false; const baz: boolean = c ? false : true; // good const foo: Interface = a || b; const bar: boolean = !!c; const baz: boolean = !c;
-
14.1 Always use braces for if/else blocks or functions with multiple statemets, and place statements on their own lines.
// bad if (test) return false; if (test) return false; // good if (test) { return false; } // bad const foo = (): boolean => { const isTrue: boolean = true; return isTrue; }; // good const bar = (): boolean => { const isTrue: boolean = true; return isTrue; }; // good const foo = (bar: boolean): boolean => bar;
-
14.2 If you're using multi-line blocks with
if
andelse
, putelse
on the same line as yourif
block's closing brace.// bad if (test) { thing1(); thing2(); } else { thing3(); } // good if (test) { thing1(); thing2(); } else { thing3(); }
-
15.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 be placed at the beginning of the line.// bad if ((foo === 123 || bar === 'abc') && doesItLookGoodWhenItBecomesThatLong() && isThisReallyHappening()) { thing1(); } // bad if ((foo === 123 || bar === "abc") && doesItLookGoodWhenItBecomesThatLong() && isThisReallyHappening()) { thing1(); } // good if ((foo === 123 || bar === "abc") && doesItLookGoodWhenItBecomesThatLong() && isThisReallyHappening()) { thing1(); }
-
16.1 Use
/** ... */
for multi-line comments.// bad // make() returns a new element // based on the passed in tag name // // @param {String} tag // @return {Element} element const make = (tag: string): Element => { // ... return element; } // good /** * make() returns a new element * based on the passed-in tag name */ const make = (tag: string): Element { // ... return element; }
-
16.2 Use
//
for single line comments. Place single line comments on a newline above the subject of the comment. Put an empty line before the comment unless it's on the first line of a block.// bad const active: boolean = true; // is current tab // good // is current tab const active: boolean = true; // bad const getType = (): string => { console.log('fetching type...'); // set the default type to 'no type' const type: string = this.type || 'no type'; return type; }; // good const getType = (): string => { console.log('fetching type...'); // set the default type to 'no type' const type: string = this.type || 'no type'; return type; }; // also good const getType = (): string => { // set the default type to 'no type' const type: string = this.type || 'no type'; return type; };
-
16.3 Start all comments with a space to make it easier to read.
// bad //is current tab const active: boolean = true; // good // is current tab const active: boolean = true; // bad /** *make() returns a new element *based on the passed-in tag name */ const make = (tag: string): Element { // ... return element; }; // good /** * make() returns a new element * based on the passed-in tag name */ const make = (tag: string): Element { // ... return element; };
-
16.4 Prefixing your comments with
TODO
helps other developers quickly understand if you're pointing out a problem that needs to be solved. Whenever possible, these comments should be supplemented with more context, like a linked story that can be specified by, for example, a JIRA issue number.class Calculator extends Abacus { private total: number; constructor() { super(); // TODO: total should be configurable by an options param - JIRA issue: UI-4312 this.total = 0; } }
-
17.1 Place 1 space before the opening parenthesis in control statements (
if
,while
etc.). Place no space between the argument list and the function name in function calls and declarations.// bad if(isJedi) { fight (); } // good if (isJedi) { fight(); } // bad fight () { console.log ('Swooosh!'); } // good fight() { console.log('Swooosh!'); }
-
17.2 Set off operators with spaces.
// bad const x: number=y+5; // good const x: number = y + 5;
-
17.3 End files with a single newline character.
// bad import { es6 } from './AirbnbStyleGuide'; // ... export es6;
// bad import { es6 } from './AirbnbStyleGuide'; // ... export es6;↵ ↵
// good import { es6 } from './AirbnbStyleGuide'; // ... export es6;↵
-
17.4 Use indentation when making long method chains (more than 2 method chains). Use a leading dot, which emphasizes that the line is a method call, not a new statement.
// bad $('#items').find('.selected').highlight().end().find('.open').updateCount(); // bad $('#items'). find('.selected'). focus(). click(); // good $('#items') .find('.selected') .focus() .click(); // good const leds: string = stage.selectAll('.led').data(data);
-
17.5 Leave a blank line after blocks and before the next statement.
// bad if (foo) { return bar; } return baz; // good if (foo) { return bar; } return baz; // bad const obj: Interface = { foo: () => {}, bar: () => {}, }; return obj; // good const obj: Interface = { foo: () => {}, bar: () => {}, }; return obj;
-
17.6 Do not pad your blocks with blank lines.
// bad bar() { console.log(foo); } // also bad if (baz) { console.log(qux); } else { console.log(foo); } // good bar() { console.log(foo); } // good if (baz) { console.log(qux); } else { console.log(foo); }
-
17.7 Do not add spaces inside parentheses.
// bad bar( foo: Foo ): Foo { return foo; } // good bar(foo: Foo): Foo { return foo; } // bad if ( foo ) { console.log(foo); } // good if (foo) { console.log(foo); }
-
17.8 Do not add spaces inside brackets.
// bad const foo: number[] = [ 1, 2, 3 ]; console.log(foo[ 0 ]); // good const foo: number[] = [1, 2, 3]; console.log(foo[0]);
-
17.9 Add spaces inside curly braces.
// bad const foo: Interface = { clark: 'kent' }; // good const foo: Interface = {clark: 'kent'};
-
17.10 Avoid having lines of code that are longer than 140 characters (including whitespace). If a line is to be broken, it should be broken at a meaningful position. If it barely exceeds 140 characters, you can leave it as is. Note: per above, long strings are exempt from this rule, and should not be broken up.
Why? This ensures readability and maintainability.
// bad const foo: Interface = jsonData && jsonData.foo && jsonData.foo.bar && jsonData.foo.bar.baz && jsonData.foo.bar.baz.quux && jsonData.foo.bar.baz.quux.xyzzy; // good const foo: Interface = jsonData && jsonData.foo && jsonData.foo.bar && jsonData.foo.bar.baz && jsonData.foo.bar.baz.quux && jsonData.foo.bar.baz.quux.xyzzy; // bad $.ajax({ method: 'POST', url: 'https://airbnb.com/', data: { name: 'John' } }).done(() => console.log('Congratulations!')).fail(() => console.log('You have failed this city.')); // good $.ajax({ method: 'POST', url: 'https://airbnb.com/', data: { name: 'John' }, }) .done(() => console.log('Congratulations!')) .fail(() => console.log('You have failed this city.'));
-
18.1 Do not use leading commas, use trailing commas.
// bad const story: Word[] = [ once , upon , aTime ]; // good const story: Word[] = [ once, upon, aTime, ]; // bad const hero: Hero = { firstName: 'Ada' , lastName: 'Lovelace' , birthYear: 1815 , superPower: 'computers' }; // good const hero: Hero = { firstName: 'Ada', lastName: 'Lovelace', birthYear: 1815, superPower: 'computers', };
-
18.2 Use the additional trailing comma.
Why? This leads to cleaner git diffs. Also, transpilers like Babel will remove the additional trailing comma in the transpiled code which means you don't have to worry about the trailing comma problem in legacy browsers.
// bad - git diff without trailing comma const hero: Hero = { firstName: 'Florence', - lastName: 'Nightingale' + lastName: 'Nightingale', + inventorOf: ['coxcomb chart', 'modern nursing'] }; // good - git diff with trailing comma const hero: Hero = { firstName: 'Florence', lastName: 'Nightingale', + inventorOf: ['coxcomb chart', 'modern nursing'], };
// bad const hero: Hero = { firstName: 'Dana', lastName: 'Scully' }; const heroes: string[] = [ 'Batman', 'Superman' ]; // good const hero: Hero = { firstName: 'Dana', lastName: 'Scully', }; const heroes: string[] = [ 'Batman', 'Superman', ]; // bad createHero( firstName: string, lastName: string, inventorOf: string[] ) { // does nothing } // good createHero( firstName: string, lastName: string, inventorOf: string[], ) { // does nothing } // bad createHero( firstName: string, lastName: string, inventorOf: string[] ); // good createHero( firstName: string, lastName: string, inventorOf: string[], ); // good (note that a comma must not appear after a "rest" element) createHero( firstName: string, lastName: string, inventorOf: string[], ...heroArgs );
-
19.1 Use semicolons to end your code statements.
// bad ((): string { const name: string = 'Skywalker' return name })() // good ((): string { const name: string = 'Skywalker'; return name; }());
- 20.1 Perform type coercion at the beginning of the statement.
-
20.2 Strings:
// => this.reviewScore = 9; // bad const totalScore: string = this.reviewScore + ''; // invokes this.reviewScore.valueOf() // bad const totalScore: string = this.reviewScore.toString(); // isn't guaranteed to return a string // good const totalScore: string = String(this.reviewScore);
-
20.3 Numbers: Use
Number
for type casting andparseInt
always with a radix for parsing strings.const inputValue: string = '4'; // bad const val: number = new Number(inputValue); // bad const val: number = +inputValue; // bad const val: number = inputValue >> 0; // bad const val: number = parseInt(inputValue); // good const val: number = Number(inputValue); // good const val: number = parseInt(inputValue, 10);
-
20.4 If for whatever reason you are doing something wild and
parseInt
is your bottleneck and need to use Bitshift for performance reasons, leave a comment explaining why and what you're doing.// good /** * parseInt was the reason my code was slow. * Bitshifting the String to coerce it to a * Number made it a lot faster. */ const val: number = inputValue >> 0;
-
20.5 Note: Be careful when using bitshift operations. Numbers are represented as 64-bit values, but bitshift operations always return a 32-bit integer (source). Bitshift can lead to unexpected behavior for integer values larger than 32 bits. Discussion. Largest signed 32-bit Int is 2,147,483,647:
2147483647 >> 0; // => 2147483647 2147483648 >> 0; // => -2147483648 2147483649 >> 0; // => -2147483647
-
20.6 Booleans:
const age: number = 0; // bad const hasAge: boolean = new Boolean(age); const hasAge: boolean = Boolean(age); // best const hasAge: boolean = !!age;
-
21.1 Be descriptive with your naming.
// bad const q = () => { // ... }; // good const query = () => { // ... };
-
21.2 Use camelCase when naming objects, functions, and instances.
// bad const OBJEcttsssss: Interface = {}; const this_is_my_object: Interface = {}; const c = () => {}; // good const thisIsMyObject: Interface = {}; const thisIsMyFunction = () => {};
-
21.3 Use PascalCase only when naming constructors or classes.
// bad class user { private name: string; constructor(options: Options) { this.name = options.name; } } const bad: user = new user({ name: 'nope', }); // good class User { private name: string; constructor(options: Options) { this.name = options.name; } } const good: User = new User({ name: 'yup', });
- 21.4 Do not use trailing or leading underscores.
- 22.1 Accessor functions for properties are not required.
-
22.2 Do use TypeScript getters/setters.
// good class Dragon { get age(): number { // ... } set age(value: number): number { // ... } }
-
23.1 When attaching data payloads to events (whether DOM events or something more proprietary like Backbone events), pass a hash instead of a raw value. This allows a subsequent contributor to add more data to the event payload without finding and updating every handler for the event. For example, instead of:
// bad $(this).trigger('listingUpdated', listing.id); // ... $(this).on('listingUpdated', (e: JQueryEvent, listingId: string) => { // do something with listingId });
prefer:
// good $(this).trigger('listingUpdated', { listingId: listing.id }); // ... $(this).on('listingUpdated', (e: JQueryEvent, data: { listingId: string; }) => { // do something with data.listingId });
-
24.1 Prefix jQuery object variables with a
$
if they are outsideui
elements bind to a Marionette View.// bad const sidebar: JQueryElement = $('.sidebar'); // good const $sidebar: JQueryElement = $('.sidebar'); // good const $sidebarBtn: JQueryElement = $('.sidebar-btn');
-
24.2 Cache jQuery lookups whenever possible.
// bad function setSidebar() { $('.sidebar').hide(); // ... $('.sidebar').css({ 'background-color': 'pink', }); } // good function setSidebar() { const $sidebar: JQueryElement = $('.sidebar'); $sidebar.hide(); // ... $sidebar.css({ 'background-color': 'pink', }); }
-
24.4 Use
find
with scoped jQuery object queries.// bad $('ul', '.sidebar').hide(); // bad $('.sidebar').find('ul').hide(); // good $('.sidebar ul').hide(); // good $('.sidebar > ul').hide(); // good $sidebar.find('ul').hide();
-
25.1 Do not use TC39 proposals that have not reached stage 3.
Why? They are not finalized, and they are subject to change or to be withdrawn entirely. We want to use JavaScript, and proposals are not JavaScript yet.
-
26.2 You should be writing tests for all new code you write. 100% test coverage is a good goal to strive for, even if it's not always practical to reach it.
Why? Testing aggressively gives you solid proofs that your system/application will work the way you want. Plus, if new code breaks your application, it will be much easier to find out why it happened if the code base is well tested.
-
26.3 Whenever you fix a bug, write a regression test. In other words, add additional unit tests proving that the bug is really fixed and unlikely to break again in the future.
Why? A bug fixed without a regression test is almost certainly going to break again in the future.
- 26.4 Use mocks to mock objects, and create them in their own files.
Why? Having your mocks outside your test files makes your tests more readable.
- 26.5 Name your spies with the name of the entity you want to spy on (be it a method or property) following with the Spy suffix. For example, if you want to spy on a method called
renderChildren
, you should name your spyrenderChildrenSpy
.Why? Naming your spies makes your tests easier to understand and shorter to write overall. Adding the Spy suffix makes the person who reads your code aware that it is a "spied upon" entity.
- 26.6 Prefer built-in jasmine matchers (
toBeDefined
,toEqual
,toBe
,toContain
, etc) before custom matchers, except fortoBeTruthy
andtoBeFalsy
.Why? Jasmine's matchers are robust and give clear information in the logs if your test breaks, which make things easier to debug.
- 27.1 Review code of your peers daily as long as there are pull requests to review. No, this won't affect your productivity negatively, it will speed it up.
Why? Receiving or giving code reviews at least daily speeds up the feedback rate for each team member and consequently speeds up the rate at which your team merges its pull requests without losing in quality. Reviewing and merging pull requests at a standard and predictible pace gives momentum to the team. Adopting this habit can also free your brain from thinking about tasks that have remained pending in a "review" state for multiple days without having received any feedback.
- 27.2 Consider a complete code review as having read (and hopefully understood) each line of code contained in the pull request being reviewed, including its unit tests. If there is a demo (in the form of a live demo, video or image), make sure you thoroughly tested it or seen it. If the pull request to review is long and you think it will take more than an hour to do, be disciplined, and do it anyway.
Why? Half a review is no review. Be thorough and genuinely critical in your review, but always remain respectful.
- 27.3 Avoid looking for small or unimportant issues. As a rule of thumb, seek "good enough" code, and not perfection (it's subjective anyway). If the code fits the standards described in this document, don't go overboard to prove a point. You can offer suggestions, but don't insist on them being implemented to approve features of your peers.
Why? Nit picking and perfectionism kills development speed.
- 28.1 Avoid letting your pull requests pending "in review" for too long. As a rule of thumb, focus on applying reviews and closing your on going pull requests before starting other features.
Why? It keeps discussions around pull requests more lively and on point. It's easy to lose some context about a pull request when you did not touch it for a couple of days.
-
28.2 Provide written explanations on what complex parts of your code do. Provide written explanations on changes you've made after receiving reviews. And, all explanations should preferably appear in the pull request itself.
Why? Explanations help everybody understand better your pull request and its advancements. Having them all in the pull request allow everyone to see the explanations (as opposed to direct messages between two team members on Slack).
- 28.3 Combat lazyness. It might sound obvious when you are fresh into a new feature. However, after a couple of days (or weeks) into one, things can get psychologically more complex. Always stay self-aware of whether you are arguing against doing something because you are tired of a feature, or really because it is not worthwile to do at this point.
Why? Regardless of the state you're in, quality should always be the first priority.
-
28.4 Avoid debating over small or unimportant issues. If, in your view, you received nit picking comments on your pull request and someone insists on you making changes, apply them quickly and move on.
Why? Sometimes things are just subjective and arguing thus becomes a time-consuming dead end. The sooner the reviewer or the reviewee recognizes the situation, the better.
- 29.1 Use
PascalCase
when naming your interfaces, andcamelCase
for their members.
- 29.2 Do not prefix your interfaces with
I
.Why? Unconventional. Important interfaces (like
Window
,Document
) are usually not defined with theI
prefix.
- 29.3 Use
PascalCase
when naming your types, andcamelCase
for their members.
- 29.4 Use
PascalCase
when naming your enums, andcamelCase
for their members.
- 29.5 Avoid specifying the returned type if the function can return anything (
any
) or nothing.
The following list is an overview of the main frameworks and libraries we use when developping for the Coveo Cloud Platform. You can dig deeper by clicking on the links of each library or framework listed:
- Backbone.js
- Marionette.js
- EJS
- React
- Redux
- Underscore.js
- jQuery
- Moment.js
- URI.js
- Polyglot.js
- Webpack
- Gulp
- npm
A few rules of thumb:
- Use "native" features of Backbone/Marionette and React/Redux whenever you can. Only defaults to using jQuery if and only if your problem cannot be solved with the main frameworks and architecture we use.
- Always use EJS for HTML templating if you are working with Backbone/Marionette.
- Use Underscore.js as much as it pleases you.
- If you are working on new components or complex features, favor React/Redux over Backbone/Marionette as your framework of choice.
As for any project that has lived for a long period of time, you may find some places in the code base where the code does not respect some standards described above. In this case, do refactor the code if it is related to your current task, or if you think it could represent a potential security or bug threat. Otherwise, you can leave the code as it is.
You can refer to the original Airbnb Style Guide for the remaining sections if you want, although they are really not a required read by our team.