A style guide for writing better typescript, enforced via eslint pre-commit hook.
Function parameters and arguments should be on the same line until they surpass the max line length(80), at which point they should be aligned vertically each on their own line.
// BAD
const bar = (a,
b,
c) => {
...
};
// GOOD
const bar = (a, b, c) => {
...
};
// BAD having to scroll horizontally is shitty
const foo = (parameter, otherReallyLongParameter, superDuperLongParameter, tooManyParametersForOneLine) => {
...
};
// GOOD
const foo = (
parameter,
otherReallyLongParameter,
superDuperLongParameter,
tooManyParametersForOneLine
) => {
...
};
Arrow Function Parentheses - Prettier
Always use parentheses around arrow function parameters, for consistency.
// BAD
const foo = x => x*x;
const lesserThings = things.map(thing => thing - 1);
// GOOD
const foo = (x) => x*x;
const lesserThings = things.map((thing) => thing -1);
fdescribe
/fit
(focusing blocks/tests silently kills test suites' usefulness)
Class and Interface names should always be pascal-case for ease of identification.
// BAD
interface user {
...
}
class accountInfo {
...
}
// GOOD
interface User {
...
}
class AccountInfo {
...
}
No leading or trailing underscores or keywords (any, Number, number, String, string, Boolean, boolean, undefined) either.
// BAD
const _pretending_im_private = false;
// GOOD
const notPretending = true;
There should be a space between the //
and the first word.
//not a pleasant
//reading experience
// a much better
// reading experience
Curly braces should always be used for if/for/do/while
statements for clarity on what is included in the statement.
// BAD
if(thing < other)
thing++;
return thing;
// it's unclear whether the author knows the return
// statement is not included in the conditional block
// GOOD
if(thing < other) {
thing++;
return thing;
}
// OR
if(thing < other) {
thing++;
}
return thing;
// BAD
if(foo) return true;
// it's much easier to scan for return statements
// when 'return' is the left-most word on the line
// GOOD
if(foo) {
return true;
}
End every file with a newline character, because otherwise it's not a line.
Mixing tabs and spaces is insanity, and spaces are interpreted the same universally whereas tabs are not.
Why mangle names to save time in differentiating an interface from a class when it's an arguably superfluous distinction in many cases.
// BAD
interface IIllusion { // kill me now please
...
}
// GOOD
interface Illusion {
...
}
Labels only belong in do/for/while/switch
statements, if at all.
// BAD
happy:
if(true) {
console.log('happy days');
if(condition) {
break happy;
}
}
// ACCEPTABLE, albeit contrived
let i = 0;
let j = 0;
loop1:
for (i = 0; i < 3; i++) {
loop2:
for (j = 0; j < 3; j++) {
if (i === 1 && j === 1) {
break loop1;
}
console.log(`i = ${i}, j = ${j}`);
}
}
Classes deserve their own files (which should be named after them).
// BAD
/* in file 'hurrdy-durr.ts' */
class Foo {
...
}
class Bar {
...
}
// GOOD
/* in file 'foo.ts' */
class Foo {
...
}
/* in file 'bar.ts' */
class Bar {
...
}
Order the members of a class consistently, for discoverability. Priority rules are:
- fields
- static public
- static private
- public
- private
- functions
- constructor
- static public
- static private
- public
- private
Members are public by default, so avoid clutter by omitting the designation.
class Alphabet {
static a = true;
static private b = false;
c = true;
private d = false;
constructor() {
...
}
static e() {
...
}
static private f() {
...
}
g() {
...
}
private h() {
...
}
}
Use parentheses when invoking a constructor function with new
for consistency with other function calls.
class Foo {
...
}
// BAD
const bad = new Foo;
// you'll have to use parentheses to pass arguments to other
// constructors, so use them all the time for consistency.
// GOOD
const good = new Foo();
Using arguments.callee
makes various performance optimizations impossible.
// BAD
[1, 2, 3, 4, 5].map(function(n) {
return !(n > 1) ? 1 : arguments.callee(n - 1) * n;
});
// GOOD
const factorial = (n) => {
return !(n > 1) ? 1 : factorial(n - 1) * n;
};
[1, 2, 3, 4, 5].map(factorial);
In most cases these are typos (i.e. foo & bar()
when meaning foo && bar()
) or overly clever/opaque. If there is a great reason for a bitwise operation, locally overriding the rule is fine.
Like bitwise operations, these are often merely typos. If used purposefully, they're harder to notice and therefore a potential for great pain and suffering.
// BAD
let foo;
if(foo = bar) { // either a typo or someone being sneaky (a.k.a. inducing headaches)
...
}
// GOOD
const foo = bar;
if(foo) {
...
}
// OR
...
if(foo === bar) {
...
}
Think of one blank line as a comma, two blank lines as a period. Three (or more) blank lines would be an exclamation point (or series of points). Exclamation points are bad.
"One should never use exclamation points in writing. It is like laughing at your own joke." -- Mark Twain
In almost every case the intention of something like new Number('0')
or new Boolean('false')
is to perform a type conversion, not to create a wrapper object.
// BAD
const condition = new Boolean('false');
if(condition) {
// this will always execute, because 'condition' is
// an object (therefore truthy) regardless of content
...
}
// GOOD
const condition = Boolean('false');
if(condition) {
...
}
Default exports have a tumultuous history (and present) with transiplation tooling, and naming all exports promotes clarity by disallowing the exporting of anonymous functions.
// BAD
export default function() {
...
};
// GOOD
export const foo = () => {
...
};
Arbitrary code execution is a no-no.
Explicitly declaring types on constants assigned primitives is needless clutter.
// BAD
const foo:boolean = true;
// GOOD
const foo = true;
let bar:number = 0; // since let allows reassignment, type assertion is valid
const bazz = (buzz:boolean = false) => {
// parameters are reassignable, so type assertion is valid (but not required)
...
};
Avoiding property mutation makes debugging easier by limiting side effects.
// BAD
const foo = {
bar: 1,
baz: true,
};
foo.bar = 2;
doSomething(foo);
// GOOD
const foo = {
bar: 1,
baz: true,
}
const updatedFoo = {
...foo,
bar: 2,
};
doSomething(updatedFoo);
You're using a transpiler that understands ES6 import syntax, so use it.
// BAD
const foo = require('foo');
// good
import { foo } from 'foo';
Wash your hands after using the bathroom, cover your mouth when you snenoeze, and don't commit trailing whitespace for other peoples' text editors to remove bloating everyone's diffs.
Unused expressions are most frequently typos.
BAD
const bar = () => {
...
};
bar; // no-op probably meant to be a function invocation
Use let
or const
for greater clarity.
If possible, avoid quotation marks around object literal keys to make them easier to read (less superfluous characters to parse).
// BAD
const foo = {
'bar': true,
};
// GOOD
const foo = {
bar: true,
'fizz-buzz': 3,
};
Reducing clutter by removing duplication.
const bar = true;
const bazz = false;
// BAD
const foo = {
bar: bar,
bazz: bazz,
};
// GOOD
const foo = {
bar,
bazz,
};
Sorted objects allow readers to visually binary search for keys, and helps prevent merge conflicts.
// BAD
const foo = {
marbles: 5,
'carrot cake': [],
xylophone: true,
boo: 'ahhhhhhh',
};
// GOOD
const foo = {
boo: 'ahhhhhhh',
'carrot cake': [],
marbles: 5,
xylophone: true,
};
The catch/finally/else
statements should all be on the same line as their preceding and following block braces with a single space separating them. All blocks should be at least three lines.
// BAD
if( ... ) { ... }
const foo = () => { return ... };
[...].map((x) => { return ... });
// starting/ending block braces on the same line
// GOOD
if( ... ) {
...
}
const foo = () => {
return ...
};
[...].map((x) => {
return ...
});
// BAD
if( ... ) {
...
}
else // not on the same line as the preceding brace or the following brace
{ ... }
// BAD
if( ... ) {
...
}else{ // no space between braces and 'else'
...
}
// GOOD
if( ... ) {
...
} else {
...
}
// BAD
try {
...
}catch(error){ ... } // starting/ending braces on the same line, no spaces around catch
finally { // not on the same line as the preceding brace
...
}
// GOOD
try {
...
} catch(error) {
...
} finally {
}
Reduce diff clutter and avoid easy typos by not chaining assignments. Not enforced in for
loops.
// BAD
const a = 5,
b = true,
c = null;
// removing 'c' requires changing the above comma to a semicolon. chaining
// 'd' after 'c' requires updating the semicolon to a comma. both operations
// pollute the diff and risk an easy typo headache.
// GOOD
const a = 5;
const b = true;
const c = null;
// delete any of the above or add another anywhere within/around and
// you'll always have a one line diff with no chance of a comma updating
// typo.
// OK
for(let a = 0, b = false, c; ... ; ...) {
...
}
(After ESlint migration - this rule changed)
Traditional anonymous functions don't bind lexical scope, so their behavior can be unexpected when referencing this
.
// BAD
const foo = {
bar: function() {
return this; // 'this' depends on the context in which 'bar' is called
}
};
// GOOD
const foo = {
bar: () => {
return this; // 'this' will always be the context in which 'foo' was defined
}
};
(After ESlint migration - rule changed)
Within groups (delineated by blank lines) imports should be alphabetized, case-insensitive.
// BAD
import { ... } from 'foo';
import { ... } from 'bar';
import { ... } from 'fizz';
import { ... } from 'buzz';
import { ... } from 'aardvark';
// GOOD
import { ... } from 'aardvark';
import { ... } from 'bar';
import { ... } from 'buzz';
import { ... } from 'fizz';
import { ... } from 'foo';
// OR
import { ... } from 'buzz';
import { ... } from 'fizz';
import { ... } from 'aardvark';
import { ... } from 'bar';
import { ... } from 'foo';
Reap the semantic benefits of your declarations; when assigning a name to a value, if that value won't/can't/shouldn't change it should use a const
declaration, not a let
declaration.
// BAD
const half = (value) => {
let divisor = 2; // gives the false impression 'divisor' might change
return value / divisor;
};
// GOOD
const half = (value) => {
const divisor = 2;
return value / divisor;
};
When iterating through an array with a for
loop, prefer the for(... of ...)
construction over for(let i =0; i < length; i++)
unless the index is used for something other than accessing items. for...of
better conveys intent.
const arr = [...];
// BAD
for(let i = 0, item; i < arr.length; i++) {
item = arr[i];
console.log(item);
}
// GOOD
for(let item of arr) {
console.log(item);
}
Single Quotation Marks For Strings - Prettier
Consistency is king, and single quotation marks are less clutter.
Always include a default case for switch statements either first (preferable) or last, not stuck between other cases.
// BAD
switch(foo) {
case bar:
return false;
case fizz:
return true;
}
// GOOD
switch(foo) {
default:
break;
case bar:
return false;
case fizz:
return true;
}
Trailing Comma (Comma dangle) - Prettier
Always include trailing commas for the last item in multi-line object and array literals. Never for single-line literals. Your diffs will thank you.
// BAD
const foo = {
a: true,
b: false
};
const merp = [
1,
2,
3
];
const bar = ({
c,
d,
e
}) => {
...
};
const ugh = { x: 1, y: 2, };
const shhh = ({ quiet, time, }) => {
...
};
// GOOD
const foo = {
a: true,
b: false,
};
const merp = [
1,
2,
3,
];
const bar = ({
c,
d,
e,
}) => {
...
};
const ugh = { x: 1, y: 2 };
const shhh = ({ quiet, time }) => {
...
}
Implicit type conversion is not your friend. Strict comparisons are easier to reason about, so be explicit with any conversions you plan to make before comparing.
const foo = '4';
const bar = 4;
// BAD
if(foo == bar) {
...
}
// GOOD
if(Number(foo) === bar) {
...
}
NaN !== NaN
, so comparing to NaN
doesn't work.
// BAD
if(foo === NaN) {
...
}
// GOOD
if(isNaN(foo)) {
...
}
Breathing Room Is Good - Prettier
Whitespace between operands, separators, and assignment precedents and antecedents promotes legibility.
// BAD
const yes = no||maybe&& 2+6;
const bar = [1,'oh god',3,false];
const foo=true;
// GOOD
const yes = no || maybe && 2 + 6;
const bar = [1, 'oh god', 3, false];
const foo = true;
Reasoning about switch statements making use of fall-through cases that aren't empty is hard, and often a case falling through is accidental.
// BAD
switch(foo) {
case a:
buzz(foo);
case b:
fizz(foo);
}
// what are the implications of buzz on foo given that
// fizz could be receiving it next? why is the author
// making us worry about that? did they even mean for
// that to be possible? do they hate us?
// GOOD
switch(foo) {
case a:
return buzz(foo);
case b:
return fizz(foo);
}
// OK
switch(foo) {
case a:
case b:
return buzz(foo);
// obvious that a and b both trigger the same behavior
case c:
return fizz(foo);
}