Code Conventions

Summary

This page covers code style guidelines and conventions.

Auto Styling

We use eslint and prettier to autoformat all Dendron code before a commit. You can see our styling options here

Conventions

The general guideline is to follow Airbnb's Javascript Style Guide. There may be a few differences since we're using Typescript instead of Javascript. Any differences or particular points of emphasis are covered in the below sections.

one export per file

See Airbnb guide 10.6

Prefer to only have one export per file, and to break functionality into different files. Classes should generally always be in their own files.

file name should match the default export exactly

See Airbnb guide 23.6

This file should be called makeStyleGuide.ts

function makeStyleGuide() {
  // ...
}

export default makeStyleGuide;

Note: We currently have a lot of utility files that have multiple exports, which means they can't follow this rule. For those files, pick as descriptive of a name as possible, and avoid having utility files with multiple exports in the future.

prefer capital snake case for constants

// good
A_SNAKE = "PYTHON"

// bad
ASNAKE = "python"

prefer using camelCase for variables

// good
const camelCaseIsPreferred = true

// bad
const CamelCaseIsPreferred = false

prefer returning objects instead of tuples

This make it easier to update return signatures and makes it explicit what is being returned

// good
const {fooValue, fooError} = foo()

// bad
const [fooValue, fooError] = foo()

prefer [is, does, has, should] prefixes for boolean usages.

This makes it clear that a given variable is a boolean. And communicates finality of expression.

// good
const isFriendly = isFriendly({person});

// bad
const friendly = friendly({person});

// could be improved. 
// 
// does NOT read as finalized value since maybeFriendly=true logically 
// still reads as 'maybe' while we want to communicate that it is in 
// fact friendly.
const maybeFriendly  = maybeFriendly({person});

prefer enum over strings

When we have a small set of constants, prefer using enums

// good
enum ReturnStatus {
  ok = "ok",
  fail = "fail",
}

function foo(): ReturnStatus {
  ...
}

// bad
function foo(): "ok"|"fail"{
  ...
}

Typescript

prefer using type over interface

  • types and interfaces are mostly interchangable, types are a bit more flexible and so we've standardized on using it
  • see official typescript docs here
// good
type Foo {

}

// bad
interface Foo {
}

avoid using @ts-ignore

Unless there is a special circumstance, avoid using this statement since it skips typechecking. Alternatives to some common use cases:

  • unused variables: prefix the variable with a _ (eg. _foo) to have typescript ignore it

prefer using async/await and Promises over callbacks

Makes code more readable

prefer using object notation for methods ith multiple arguments

This makes code easier to refactor

// good
function foo(opts: {arg1: string, arg2: string}) {

}
// bad
function foo(arg1: string, arg2: string) {
}

prefer undefined and null over implicitly incorrect values

Type checking can warn you about a potentially undefined value, but not a value that's just implied to be undefined like "" or -1.

//good
function findSomething(...) {
  if (...) return 4;
  return undefined; // not found!
}
// bad
function findSomething(...) {
  if (...) return 4;
  return -1; // not found!
}

prefer compile time checks for exhaustive patterns

If you have a switch or a chain of if ... else if statements where you check all possible cases, add a static assertion so that if a revision breaks this in the future it will be easily caught.

import { assertUnreachable } from "@dendronhq/common-all";

type MyOptions = "one" | "two";

// bad
function myFunction(var: MyOptions) {
  if (var === "one") {/* ... */}
  else {/* ... */}
}

// good
function myFunction(var: MyOptions) {
  if (var === "one") {/* ... */}
  else if (var === "two") {/* ... */}
  else assertUnreachable(var);
}

This works with properties within objects (e.g. node.type) too! One hint is that if the type of the object shows up as never in the editor, then you can use this assertion.

prefer forEach when iterating through an array

This is more concise and avoids some unexpected behaviors that can arise from other methods of iteration.

// good
[1,2,3].forEach(elem => console.log(elem))

prefer TS getter and setter syntax

When possible, use getter and setter syntax.

private _foo: string;

// bad
getMyProperty(): string {
    return this._foo;
}

setMyProperty(val: string) {
    this._foo = val;
}

// good
get myProperty(): string {
    return this._foo;
}

set myProperty(val: string) {
    this._foo = val;
}

Regular Expressions

Prefer negated sets

\w matches ASCII alphanumeric characters only, this means any unicode characters will not match.

> "oo".match(/[\w]+/)
[ 'oo', index: 0, input: 'oo', groups: undefined ]
> "öö".match(/[\w]+/)
null

As a result, describing the set of characters that should match is practically impossible if you want unicode characters to match as well. Instead, use a negated set to describe which should not match.

> "öö".match(/[^\s]+/)
[ 'öö', index: 0, input: 'öö', groups: undefined ]

Async operations

If you have many async operations to perform, decide if they need to be done in parallel or in serial.

In parallel

const outputs = await Promise.all(
  vaults.map(async () => {
    // Do async thing here
  })
);

In series

Use asyncLoopOneAtATime

// this creates directories 'one' and 'two' 
await asyncLoopOneAtATime<string>(["one", "two"], (ent) => {
    return fs.ensureDir(ent);
});

Time

We use luxon for all time related tasks. This is the successor of moment.js


Children
  1. Best Practices
  2. Checklist
  3. Code Utilities and how to find them
  4. Config
  5. Cook
  6. Dev Env
  7. Git
  8. Gotchas
  9. Monorepo
  10. React

Backlinks