/coffeescript-style-guide

Apiary's best-practices and coding conventions for the CoffeeScript programming language

Apiary CoffeeScript Style Guide

This guide presents a collection of best-practices and coding conventions for the CoffeeScript programming language.

It intentionally diverges a bit from idioms, such as optional function parentheses or use the coolest available syntactic sugar to write it all on just a single line. Aim of this style guide is to be closer to Python than to Perl and to avoid some readability problems.

Please note that this is a work-in-progress. Use standard GitHub flow for issuing objections, ideas, contributions. Style Guide should be based on consensus, so your ideas are very welcome.

Inspiration

The details in this guide have been very heavily inspired by several existing style guides and other resources. In particular:

Table of Contents

Code layout

Tabs or Spaces?

Use spaces only, with 2 spaces per indentation level. Never mix tabs and spaces.

Maximum Line Length

Limit all lines to a maximum of 79 characters.

Blank Lines

Separate top-level function and class definitions with two blank lines.

Separate method definitions inside of a class with a single blank line.

Use a single blank line within the bodies of methods or functions in cases where this improves readability (e.g., for the purpose of delineating logical sections).

In tests, separate major blocks (i.e., Feature, Scenario, describe) by two blank lines.

Trailing Whitespace

Do not include trailing whitespace on any lines.

Optional Commas

Avoid the use of commas before newlines when properties or elements of an Object or Array are listed on separate lines.

# Yes
foo = [
  'some'
  'string'
  'values'
]
bar:
  label: 'test'
  value: 87

# No
foo = [
  'some',
  'string',
  'values'
]
bar:
  label: 'test',
  value: 87

Always use commas to separate function arguments:

# Yes
moveTo(10,
  key: value
)

# No
moveTo(10
  key: value
)

Optional Braces

Avoid use of braces in multi-line object literals:

# Yes
value =
  color: 'blue'
  size: 42

# No
value = {
  color: 'blue'
  size: 42
}

Use braces in case you need implicit key-value pairs:

# Yes
value = {
  color: 'blue'
  size
}

# No
value = {
  color: 'blue'
  size: 42
}

Always use braces in one-line object literals:

{color: 'blue', size: 42} # Yes
color: 'blue', size: 42 # No

Avoid extraneous whitespace in one-line object literals:

{color: 'blue', size: 42} # Yes
{ color: 'blue', size: 42 } # No

Encoding

UTF-8 is the only allowed source file encoding.

Module Imports

If using a module system (CommonJS Modules, AMD, etc.), require statements should be placed on separate lines.

require('lib/setup')
Backbone = require('backbone')

These statements should be grouped in the following order:

  1. Standard library imports (if a standard library exists) and third party library imports
  2. Local imports (imports specific to this application or library)

Groups should be separated by one blank line from each other and by two blank lines from the rest of the file.

Do not use .coffee extension while specifying path to a local module.

mod = require('module') # Yes
mod = require('module.coffee') # No

Whitespace in Expressions and Statements

Avoid extraneous whitespace in the following situations:

  • Immediately inside parentheses, brackets or braces

       $('body') # Yes
       $( 'body' ) # No
  • Immediately before a comma

       console.log(x, y) # Yes
       console.log(x , y) # No

Additional recommendations:

  • Always surround these binary operators with a single space on either side

    • assignment: =

      • Note that this also applies when indicating default parameter value(s) in a function declaration

        test: (param = null) -> # Yes
        test: (param=null) -> # No
    • augmented assignment: +=, -=, etc.

    • comparisons: ==, <, >, <=, >=, unless, etc.

    • arithmetic operators: +, -, *, /, etc.

    • (Do not use more than one space around these operators)

         # Yes
         x = 1
         y = 1
         fooBar = 3
      
         # No
         x      = 1
         y      = 1
         fooBar = 3

Comments

If modifying code that is described by an existing comment, update the comment such that it accurately reflects the new code. (Ideally, improve the code to obviate the need for the comment, and delete the comment entirely.)

The first word of the comment should be capitalized, unless the first word is an identifier that begins with a lower-case letter.

If a comment is short, the period at the end can be omitted.

Block Comments

Block comments apply to the block of code that follows them.

Each line of a block comment starts with a # and a single space, and should be indented at the same level of the code that it describes.

Paragraphs inside of block comments are separated by a line containing a single #.

  # This is a block comment. Note that if this were a real block
  # comment, we would actually be describing the proceeding code.
  #
  # This is the second paragraph of the same block comment. Note
  # that this paragraph was separated from the previous paragraph
  # by a line containing a single comment character.

  init()
  start()
  stop()

Inline Comments

Inline comments are placed on the line immediately above the statement that they are describing. If the inline comment is sufficiently short, it can be placed on the same line as the statement (separated by a single space from the end of the statement).

All inline comments should start with a # and a single space.

The use of inline comments should be limited, because their existence is typically a sign of a code smell.

Do not use inline comments when they state the obvious:

  # No
  x = x + 1 # Increment x

However, inline comments can be useful in certain scenarios:

  # Yes
  x = x + 1 # Compensate for border

Naming Conventions

Use camelCase (with a leading lowercase character) to name all variables, methods, and object properties.

Use CamelCase (with a leading uppercase character) to name all classes. (This style is also commonly referred to as PascalCase, CamelCaps, or CapWords, among other alternatives.)

(The official CoffeeScript convention is camelcase, because this simplifies interoperability with JavaScript. For more on this decision, see here.)

For constants, use all uppercase with underscores:

CONSTANT_LIKE_THIS

Methods and variables that are intended to be "private" should begin with a leading underscore:

_privateMethod: ->

File names

Use dashes for naming files:

user-rights.coffee
private-traffic.coffee

Use -test suffixes for naming test files:

tests/user-rights-test.coffee # Yes
tests/user-rights.coffee # No
tests/user_rights_test.coffee # No

The word test is a good visual lead in editors and it also helps to distinguish files containing actual tests from files with helpers and shared behaviors.

Functions

(These guidelines also apply to the methods of a class.)

When declaring a function that takes arguments, always use a single space after the closing parenthesis of the arguments list:

foo = (arg1, arg2) -> # Yes
foo = (arg1, arg2)-> # No

Do not use parentheses when declaring procedures (functions that take no arguments):

bar = -> # Yes
bar = () -> # No

In cases where method calls are being chained and the code does not fit on a single line, each call should be placed on a separate line and indented by one level (i.e., two spaces), with a leading ..

[1..3]
  .map((x) -> x * x)
  .concat([10..12])
  .filter((x) -> x < 11)
  .reduce((x, y) -> x + y)

When calling functions, never omit parentheses. It is a strict and strong rule, but it deals with significant majority of ambiguous cases and problems you can find in CoffeeScript:

baz(12)

brush.ellipse({x: 10, y: 20})

foo(4).bar(8)

obj.value(10, 20) / obj.value(20, 10)

print(inspect(value))

new Tag(new Value(a, b), new Arg(c))

Note: As mandatory parentheses are the most emotional topic in this style guide, let's provide some background for the decision. Having mandatory parentheses around function arguments has...

Cons

  • more typing
  • not idiomatic CoffeeScript
  • less cool

Pros

  • unambiguous syntax (maintainable, readable, less mistakes)
  • you would need to use the space character anyway so it's not actually that more typing
  • the biggest CoffeeScript codebases (e.g. Atom, Quill) made the same decision
  • you'd need to type them in JavaScript (or almost any other language of this sort) anyway, syntax closer to ES6

Do not use grouping functions instead of grouping parameters:

$('#selektor').addClass('klass') # Yes
($ '#selektor').addClass 'klass' # No

foo(4).bar(8) # Yes
(foo 4).bar 8 # No

Use parentheses and commas properly when writing nested function definitions:

fn(42, 'string', (err) ->
  console.error err.message
, (arg1, arg2) ->
  arg1 + arg2
)

Do not use inline functions unless they're very short and simple:

# Yes
(x) -> x + x

# Yes
(x) ->
  if true
    42
  else
    x + x

# No
(x) -> if true then 42 else (x + x)

Deconstructions

Keep deconstructions one-line unless they are too many:

# Yes
{one, two} = object

# Yes
{
  one
  two
  three
  four
  five
  six
} = object

# No
{
  one
  two
} = object

If there are too many deconstructed elements, consider not using deconstruction at all:

# Yes
object.one
object.four
...

# No
{
  one
  two
  three
  four
  five
  six
} = object

The same rules apply to array deconstructions.

Strings

Use string interpolation instead of string concatenation:

"this is an #{adjective} string" # Yes
"this is an " + adjective + " string" # No

Prefer single quoted strings ('') instead of double quoted ("") strings, unless features like string interpolation are being used for the given string.

Use string blocks for large strings:

str = '''
  Very long block of text, which has multiple lines.

  Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam vel
  finibus ante. Morbi venenatis ante lacus, vitae rutrum odio pharetra
  vitae. Quisque mollis augue ac nisl dignissim pulvinar. Etiam gravida
  viverra sollicitudin. Praesent eu ex ultrices, vulputate lectus eu,
  laoreet nisl.
'''

Mind that CoffeeScript supports indentation of such strings for better readability:

if condition
  fn = (arg1, arg2) ->
    str = '''
      Very long block of text, which has multiple lines.

      Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam
      vel finibus ante. Morbi venenatis ante lacus, vitae rutrum odio
      pharetra vitae. Quisque mollis augue ac nisl dignissim pulvinar.
      Etiam gravida viverra sollicitudin. Praesent eu ex ultrices,
      vulputate lectus eu, laoreet nisl.
    '''

Conditionals

Favor unless over if for negative conditions, but preferably only for short, almost one-line conditionals.

Instead of using unless...else, use if...else:

  # Yes
  if true
    ...
  else
    ...

  # No
  unless false
    ...
  else
    ...

Multi-line if/else clauses should use indentation:

  # Yes
  if true
    ...
  else
    ...

  # No
  if true then ...
  else ...

One-line if/then/else conditionals should be used cautiously, preferably only for very short and simple conditions in assignments:

value = if true then 42 else 0 # Yes

if true then value = (42 or 38) else value = (5 unless false) or true # No

One-line trailing conditionals should be used cautiously, preferably only for very short and simple conditions:

return cb(err) if err # Yes

value = k for k, v of object unless err # No

Avoid complex conditionals as much as you can (when relying on lazy evaluation of the conditional, such rule can be tricky). Use named conditions:

# Yes
isLarge = thing > 42 and ...
isGreen = thing.color is 'green' or ...

if isLarge and isGreen
  ...

# No
if (thing > 42 and ...) and (thing.color is 'green' or ...)
  ...

Looping and Comprehensions

Take advantage of comprehensions whenever possible:

  # Yes
  result = (item.name for item in array)

  # No
  results = []
  for item in array
    results.push item.name

To filter:

result = (item for item in array when item.name is "test")

To iterate over the keys and values of objects:

object = {one: 1, two: 2}
alert("#{key} = #{value}") for key, value of object

As CoffeeScript doesn't support multi-line comprehentions, in case your comprehension is too long for one line, use proper loop instead:

# Yes
values = []
for keySomething, valueSomething of object.nestedObject.nestedArray
  if keySomething > 42
    values.push valueSomething

# No
values = (valueSomething for keySomething, valueSomething of object.nestedObject.nestedArray when keySomething > 42)

Use inline loops only for very short and simple constructs:

# Yes
object[v] = k for k, v of data

# No
(if val.length then arr.push (i for i, j of val)) for val in things

Extending Native Objects

Do not modify native objects.

For example, do not modify Array.prototype to introduce Array#forEach.

Exceptions

Do not suppress exceptions.

Never throw exceptions in asynchronous flow. Use first callback parameter for passing an error object.

Annotations

Use annotations when necessary to describe a specific action that must be taken against the indicated block of code.

Write the annotation on the line immediately above the code that the annotation is describing.

The annotation keyword should be followed by a colon and a space, and a descriptive note.

  # FIXME: The client's current state should *not* affect payload processing.
  resetClientState()
  processPayload()

If multiple lines are required by the description, indent subsequent lines with two spaces:

  # TODO: Ensure that the value returned by this call falls within a certain
  #   range, or throw an exception.
  analyze()

Annotation types:

  • TODO: describe missing functionality that should be added at a later date
  • FIXME: describe broken code that must be fixed
  • OPTIMIZE: describe code that is inefficient and may become a bottleneck
  • HACK: describe the use of a questionable (or ingenious) coding practice
  • REVIEW: describe code that should be reviewed to confirm implementation

If a custom annotation is required, the annotation should be documented in the project's README.

Idioms

From Zen of Python:

  • Beautiful is better than ugly.
  • Explicit is better than implicit.
  • Simple is better than complex.
  • Complex is better than complicated.
  • Flat is better than nested.
  • Sparse is better than dense.
  • Readability counts.
  • Special cases aren't special enough to break the rules.
  • Although practicality beats purity.
  • If the implementation is hard to explain, it's a bad idea.
  • If the implementation is easy to explain, it may be a good idea.

And more:

  • Consistency counts.
  • DRY counts.

Readability

Strive for readability. Your code is going to be written once, but read many, many times.

Too much syntactic sugar causes diabetes. Don't use something just because it's cool. Never use something in case you even slightly hesitate that you may be the only person in the company being aware of how it works:

included = 'a long test string'.indexOf('test') isnt -1 # Yes
included = !!~ 'a long test string'.indexOf 'test' # No

Ignore those parts of other guidelines which recommend to go against readability (i.e., The Little Book on CoffeeScript).

DRY

Strive for DRY, but mind that premature DRY is the same evil as premature optimization.

Asynchronous flow

Unless specified otherwise, handle asynchronous flow using async.js library instead of nested callbacks:

# Yes
async.waterfall [
  (next) -> ...
  (result, next) -> ...
  (result, next) -> ...
], cb

# No
fn((err, result) ->
  return cb(err) if err
  ...
)

Consider more than one statement return cb(err) if err as code smell. Do not nest more than one asynchronous function, always use async.js instead for a flow consisting two or more functions.

Use cb as a name for callbacks and err as a name for errors:

# Yes
(err, results) ->
  cb err

# No
(error, results) ->
  callback error

Use next as a name for nested callbacks if needed:

# Yes
(args, cb) ->
  async.waterfall [
    (next) -> ...
    (result, next) -> ...
    (result, next) -> ...
  ], cb

# No
(args, cb) ->
  async.waterfall [
    (done) -> ... # or whatever other name
  ], cb

Use done as a name for callbacks in tests:

# Yes
before (done) ->
  ...

# No
before (next) ->
  ...

If you run out of names and you need one more name for callback variable on top of cb and next, then your flow is too complex and you should split it into multiple functions.

# Yes
fn = (cb) ->
  ...

flow = (args, cb) ->
  async.waterfall [
    (next) -> fn(next)
  ], cb

# No
flow = (args, cb) ->
  async.waterfall [
    (next) ->
      ...
        (done) ->
          ...
            (callback) ->
              ...
  ], cb

Filling gaps in standard library

Use Underscore.js when you're missing some very basic features (e.g., for object and array manipulation) and they're not present in standard library.

Miscellaneous

and is preferred over &&.

or is preferred over ||.

is is preferred over ==.

isnt is preferred over !=.

not is preferred over !.

?= should be used when possible:

temp ?= {} # Yes
temp = temp or {} # No

Prefer shorthand notation (::) for accessing an object's prototype:

Array::slice # Yes
Array.prototype.slice # No

Prefer @property over this.property.

return @property # Yes
return this.property # No

For consistency, use also standalone @:

return @ # Yes
return this # No

Avoid return where not required, unless the explicit return increases clarity.

Use splats (...) when working with functions that accept variable numbers of arguments:

console.log args... # Yes

(a, b, c, rest...) -> # Yes

Optimization

Use explicit empty return statements to preserve performance in case large objects would be returned unnecessarily (e.g., results of loops).

Use literal syntax where applicable. Use built-ins if they're available.

Use Array::join for programatically building up a string.