List of habits/opinions I have when writing JavaScript/TypeScript. Some of these are nits, some are just generally good practice. Regardless, they are all things I feel strongly about.
- Double or single quotes.
- Tabs vs spaces.
- Avoid
null
, never willingly pick it, preferundefined
. Only use it when interfacing with other APIs. (RegExp.prototype.match
for example.) Developers seem to love supporting both. If something does expectnull
, use strict equality to check it.
// GOOD
if (maybeFrames === undefined && stack !== undefined) {
frames = parseStackString(stack);
}
// BAD
if (maybeFrames == null && stack != null) {
frames = parseStackString(stack);
}
- Always put
undefined
first in a union:
// GOOD
let value: undefined | string;
// BAD
let value: string | undefined;
- Prefer private properties and methods.
// GOOD
class Foo {
#bar() {}
}
// BAD
class Foo {
private bar() {}
}
- Always declare the accessibility of every class method.
// BAD
class Person {
getAge(): number {}
generateName(): string {}
}
// GOOD
class Person {
public getAge(): number {}
protected generateName(): string {}
}
- Always explicitly type public methods.
// BAD
class Foo {
bar() {}
}
// GOOD
class Foo {
public bar() {}
}
- Very rarely use ternaries.
// GOOD: Mostly use them for functions with only a return statement. eg:
function normalizeString(value: unknown): undefined | string {
return typeof value === 'string' ? value : undefined;
}
// BAD: Generated by ChatGPT, very nice.
const result = condition1 ? (
condition2 ? (
condition3 ? (
condition4 ? 'Result 1' : 'Result 2'
) : (
condition5 ? (
condition6 ? 'Result 3' : (
condition7 ? 'Result 4' : 'Result 5'
)
) : 'Result 6'
)
) : (
condition8 ? 'Result 7' : (
condition9 ? (
condition10 ? 'Result 8' : (
condition11 ? 'Result 9' : 'Result 10'
)
) : 'Result 11'
)
)
) : 'Result 12';
- Avoid
map
,forEach
,filter
, andevery
. I have found that you end up chaining them, and it’s almost always more maintainable, clearer, and more performant to write a manual loop.
// BAD
arr.filter((elem) => typeof elem === 'string).map((elem) => elem.trim());
// GOOD
for (const elem of arr) {
if (typeof elem === 'string') {
stack.push(elem.trim());
}
}
forEach
is disgusting. This was in the previous item but it deserves a dedicated section because I hate it so much.
// BAD: Stop reinventing `continue` and `break` with `return`.
arr.forEach((elem) => {
if (elem === undefined) {
return;
}
dispatch(elem);
});
// GOOD
for (const elem of arr) {
if (elem === undefined) {
continue;
}
dispatch(elem);
}
- Always use explicit comparisons and never rely on boolean coercion. It’s much safer and nicer to be explicit about your data types and what you’re refining away.
// BAD
if (!user) {}
if (user.enabled) {}
if (calback) {}
// GOOD
if (user !== undefined) {}
if (user.enabled === true) {}
if (calback !== undefined) {}
- Avoid defaults in destructuring. I find it difficult to mentally parse.
// BAD: Not visually clear.
const {foo = false} = opts;
// GOOD
const foo = opts.foo ?? false;
- Prefer namespace imports. Only use named imports if their names are already namespaced so it’s clear what it’s referring to.
// BAD: Confusing, pollutes module scope.
import { spawn } from 'node:child_process';
// GOOD: Any competent bundler will inline and optimize away the namespace.
import * as cp from 'node:child_process';
cp.spawn;
- Don't use objects as maps
// BAD
const obj = {};
obj[key] = value;
// GOOD
const obj = new Map();
obj.set(key, value);
- Function declarations over const arrow expressions. Not only does it look nicer with less syntactic noise, the hoisting makes things a lot easier. You are able to move functions around in your code without it breaking.
// BAD
const doTheThing = (name: string) => {};
// GOOD
function doTheThing() {}
- Avoid nested destructuring. I don’t generally use it at all but nested destructuring is disgusting.
// BAD: wtf?
const {
props: { dialogue, editor, events, mapName, onAction, pan, paused },
state: { animationConfig, animations, behavior, lastActionResponse, map },
} = this;
// Better
const { dialogue, editor, events, mapName, onAction, pan, paused } = this.props;
const { animationConfig, animations, behavior, lastActionResponse, map } = this.state;
- Don't use
.filter(Boolean)
. I do not understand the infatuation with this pattern. Not only is boolean coercion bad, I never have an array with holes like that in the first place. Maybe it’s because I avoidmap
where you’re forced to return a value.
// BAD
arr.filter(Boolean);
// GOOD
// Just don't force yourself into a scenario where it's useful.
- Bail out with small cheap checks. Sometimes a small check can avoid doing a ton of work.
// GOOD
function process(queue, rawOpts) {
if (queue.length === 0) {
return;
}
const opts = normalizeOptions(rawOpts);
for (const item of queue) {
processItem(item, opts);
}
}
- Avoid destructuring in function parameters when you have a lot of properties. Put it in a const instead in function body.
// BAD
async function writeStaticAssets({
files,
params,
config,
outDir,
address,
projectRoot,
}: {
files: FileTarget[];
params: URLParams;
config: ConfigT;
outDir: string;
address: string;
projectRoot: string;
}) {}
// GOOD
async function writeStaticAssets(opts: {
files: FileTarget[];
params: URLParams;
config: ConfigT;
outDir: string;
address: string;
projectRoot: string;
}) {
const { files, params, config, outDir, address, projectRoot } = opts;
}
- Try to avoid optional chaining except in very specific cases. I find it difficult to know where I need to place the
?
and what combination of random symbols I need to optionally call something. It also encourages sloppy typing and initialization.