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

Time

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

Conventions

prefer capital snake case for constants

// good
A_SNAKE = "PYTHON"

// bad
ANSAKE = "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))

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);
});

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

Backlinks