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.
// BADconstbar=(a,b,c)=>{ ... };// GOODconstbar=(a,b,c)=>{ ... };// BAD having to scroll horizontally is shittyconstfoo=(parameter,otherReallyLongParameter,superDuperLongParameter,tooManyParametersForOneLine)=>{ ... };// GOODconstfoo=(parameter,otherReallyLongParameter,superDuperLongParameter,tooManyParametersForOneLine)=>{ ... };Arrow Function Parentheses - Prettier
Always use parentheses around arrow function parameters, for consistency.
// BADconstfoo=x=>x*x;constlesserThings=things.map(thing=>thing-1);// GOODconstfoo=(x)=>x*x;constlesserThings=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.
// BADinterfaceuser{ ... }classaccountInfo{ ... }// GOODinterfaceUser{ ... }classAccountInfo{ ... }No leading or trailing underscores or keywords (any, Number, number, String, string, Boolean, boolean, undefined) either.
// BADconst_pretending_im_private=false;// GOODconstnotPretending=true;There should be a space between the // and the first word.
//not a pleasant//reading experience// a much better// reading experienceCurly braces should always be used for if/for/do/while statements for clarity on what is included in the statement.
// BADif(thing<other)thing++;returnthing;// it's unclear whether the author knows the return// statement is not included in the conditional block// GOODif(thing<other){thing++;returnthing;}// ORif(thing<other){thing++;}returnthing;// BADif(foo)returntrue;// it's much easier to scan for return statements// when 'return' is the left-most word on the line// GOODif(foo){returntrue;}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.
// BADinterfaceIIllusion{// kill me now please ... }// GOODinterfaceIllusion{ ... }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 contrivedleti=0;letj=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' */classFoo{ ... }classBar{ ... }// GOOD/* in file 'foo.ts' */classFoo{ ... }/* in file 'bar.ts' */classBar{ ... }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.
classAlphabet{statica=true;staticprivateb=false;c=true;privated=false;constructor(){ ... }statice(){ ... }staticprivatef(){ ... }g(){ ... }privateh(){ ... }}Use parentheses when invoking a constructor function with new for consistency with other function calls.
classFoo{ ... }// BADconstbad=newFoo;// you'll have to use parentheses to pass arguments to other// constructors, so use them all the time for consistency.// GOODconstgood=newFoo();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;});// GOODconstfactorial=(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.
// BADletfoo;if(foo=bar){// either a typo or someone being sneaky (a.k.a. inducing headaches) ... }// GOODconstfoo=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.
// BADconstcondition=newBoolean('false');if(condition){// this will always execute, because 'condition' is// an object (therefore truthy) regardless of content ... }// GOODconstcondition=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.
// BADexportdefaultfunction(){ ... };// GOODexportconstfoo=()=>{ ... };Arbitrary code execution is a no-no.
Explicitly declaring types on constants assigned primitives is needless clutter.
// BADconstfoo:boolean=true;// GOODconstfoo=true;letbar:number=0;// since let allows reassignment, type assertion is validconstbazz=(buzz:boolean=false)=>{// parameters are reassignable, so type assertion is valid (but not required) ... };Avoiding property mutation makes debugging easier by limiting side effects.
// BADconstfoo={bar: 1,baz: true,};foo.bar=2;doSomething(foo);// GOODconstfoo={bar: 1,baz: true,}constupdatedFoo={ ...foo,bar: 2,};doSomething(updatedFoo);You're using a transpiler that understands ES6 import syntax, so use it.
// BADconstfoo=require('foo');// goodimport{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.
BADconstbar=()=>{ ... };bar;// no-op probably meant to be a function invocationUse 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).
// BADconstfoo={'bar': true,};// GOODconstfoo={bar: true,'fizz-buzz': 3,};Reducing clutter by removing duplication.
constbar=true;constbazz=false;// BADconstfoo={bar: bar,bazz: bazz,};// GOODconstfoo={ bar, bazz,};Sorted objects allow readers to visually binary search for keys, and helps prevent merge conflicts.
// BADconstfoo={marbles: 5,'carrot cake': [],xylophone: true,boo: 'ahhhhhhh',};// GOODconstfoo={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.
// BADif( ... ){ ... }constfoo=()=>{return ... };[...].map((x)=>{return ... });// starting/ending block braces on the same line// GOODif( ... ){ ... }constfoo=()=>{return ... };[...].map((x)=>{return ... });// BADif( ... ){ ... }else// not on the same line as the preceding brace or the following brace{ ... }// BADif( ... ){ ... }else{// no space between braces and 'else' ... }// GOODif( ... ){ ... }else{ ... }// BADtry{ ... }catch(error){ ... }// starting/ending braces on the same line, no spaces around catchfinally{// not on the same line as the preceding brace ... }// GOODtry{ ... }catch(error){ ... }finally{}Reduce diff clutter and avoid easy typos by not chaining assignments. Not enforced in for loops.
// BADconsta=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.// GOODconsta=5;constb=true;constc=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.// OKfor(leta=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.
// BADconstfoo={bar: function(){returnthis;// 'this' depends on the context in which 'bar' is called}};// GOODconstfoo={bar: ()=>{returnthis;// '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.
// BADimport{ ... }from'foo';import{ ... }from'bar';import{ ... }from'fizz';import{ ... }from'buzz';import{ ... }from'aardvark';// GOODimport{ ... }from'aardvark';import{ ... }from'bar';import{ ... }from'buzz';import{ ... }from'fizz';import{ ... }from'foo';// ORimport{ ... }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.
// BADconsthalf=(value)=>{letdivisor=2;// gives the false impression 'divisor' might changereturnvalue/divisor;};// GOODconsthalf=(value)=>{constdivisor=2;returnvalue/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.
constarr=[...];// BADfor(leti=0,item;i<arr.length;i++){item=arr[i];console.log(item);}// GOODfor(letitemofarr){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.
// BADswitch(foo){casebar: returnfalse;casefizz: returntrue;}// GOODswitch(foo){default: break;casebar: returnfalse;casefizz: returntrue;}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.
// BADconstfoo={a: true,b: false};constmerp=[1,2,3];constbar=({ c, d, e })=>{ ... };constugh={x: 1,y: 2,};constshhh=({ quiet, time,})=>{ ... };// GOODconstfoo={a: true,b: false,};constmerp=[1,2,3,];constbar=({ c, d, e,})=>{ ... };constugh={x: 1,y: 2};constshhh=({ 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.
constfoo='4';constbar=4;// BADif(foo==bar){ ... }// GOODif(Number(foo)===bar){ ... }NaN !== NaN, so comparing to NaN doesn't work.
// BADif(foo===NaN){ ... }// GOODif(isNaN(foo)){ ... }Breathing Room Is Good - Prettier
Whitespace between operands, separators, and assignment precedents and antecedents promotes legibility.
// BADconstyes=no||maybe&&2+6;constbar=[1,'oh god',3,false];constfoo=true;// GOODconstyes=no||maybe&&2+6;constbar=[1,'oh god',3,false];constfoo=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.
// BADswitch(foo){casea: buzz(foo);caseb: 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?// GOODswitch(foo){casea: returnbuzz(foo);caseb: returnfizz(foo);}// OKswitch(foo){casea: caseb: returnbuzz(foo);// obvious that a and b both trigger the same behaviorcasec: returnfizz(foo);}