JavaScript/TypeScript

Practices and patterns for JavaScript/TypeScript in general

JavaScript

Named Exports

Always use named exports rather than export default. Named exports allow easy refactoring throughout the codebase through the use of symbols later rather than requiring you to rename instances of your component/hook/lib file by hand. It's also just one less decision to make.

Prefer const to let

When declaring a variable, it should be a const by default, and only a let if the variable will be re-assigned at some point in the function. That way, a let is a clear signal that there is re-assignment somewhere in the function body, and otherwise you can assume the variable will never change its reference.

Variable casing

camelCase: variables, functions

PascalCase: Classes, Components

UPPER_CASE: module-level constants

Pure functions

When practical, try pulling the core business logic for a process into a pure function, which doesn't fetch or save data and is focused just on transforming data and returning a response. That pure function is then easy to unit test in isolation, and allows the calling stateful function to be focused purely on fetching and storing data, rather than mixing up the two concerns.

Extracting functions

True "self-documenting" code (which is so clear that it doesn't need comments) is probably impossible, and definitely unlikely. That said, a good rule of thumb for when to extract a function is if you feel the need to write a comment describing a block of logic, you should consider taking that comment, turning it into a function name, and wrapping the code it describes in a function.

This allows the reader of the code to take a high-level pass through the steps involved in the parent function, and only dive into the details when needed. It also limits the scope of variables used in the function to the places they may actually be used.

Limiting variable scope

That brings up another best practice, which is to limit variable scope as much as possible. Once a variable is declared, it could be used anywhere below the declaration within the function. So when reading through a long function, once you see a variable, you have to look through the rest of the code to see what might be using it, mutating it, or calling it. If the variable is only used immediately and then never again, that's extra unnecessary mental overhead. It also means that if later a you declare a callback function, the function will unnecessarily copy the variable into its closure.

So, it's good practice to try to limit the scope of variables. That means only declaring them right before they're used, and if a set of variables are only used for one calculation, extracting the variables and calculation into a separate function.

Arrow Function Declarations

Post-ES2015, JavaScript has two ways of declaring functions - traditional function declarations (function foo() {} and arrow functions const foo = () => {}. They both have benefits - function declarations are hoisted to the top of the file, and don't require extra code transpiling for older browsers. Arrow functions have more predictable this binding, and are a little shorter.

To make it easier to search the codebase for function definitions, and remove a decision point when programming, it's nice to settle on a single function declaration standard, and in general, Illust code uses arrow functions.

Exceptions

Two exceptions to this rule are class methods (arrow don't get shared between instances on the prototype), and generic functions:

// Compiler error in a tsx file
const foo = <T>(x: T) => x
// Compiles
function foo<T>(x: T) => x

TypeScript

Prefer interface to type

When declaring a type, you can use either the type or interface keywords. In general interface is a little more performant, so use that unless you are doing a union type.

Prefer string const types to enums

When a value can only take a certain set of values (maybe a "type" field from a select box), you can either use a enum, or a string const type (type Kind = 'foo' | 'bar'). String consts are generally easier to work with, just as typesafe, and require less importing.

As const

When using string const types, you may sometimes want to pass in a valid string, but typescript gives you a type error anyway.

In this case, instead of explicltly casing (like 'foo' as 'foo'), you can tell TypeScript that you mean the string as a constant ('foo' as const)

Discriminated Unions

One useful technique in TypeScript is the Discriminated Union. This allows you to declare that an object may have varying values based on some key "type" value, and then use an if or case statement to know which values are available. Example:

interface NameEvent {
  kind: 'name';
  firstName: string;
  lastName: string;
}

interface AgeEvent {
  kind: 'age';
  age: number;
}

type PersonEvent = NameEvent | AgeEvent;

const addEventTopPerson = (person, event: PersonEvent) => {
  // TypeScript doesn't know which kind of event it is yet.
  if (event.kind === 'name') {
    // Typescript now knows that firstName and lastName exist and are strings.
    person.fullName = [event.firstName, event.lastName].join(' ');
  } else if (event.kind === 'age') {
    person.age = event.age;
  }
}

TsDoc comments

When creating a function or component, particularly one that will be used outside the file it is declared in, it's useful to add a TsDoc comment describing what it does. These docs will show in in the VSCode hover text for the variable, which make it easier to remember what a value means when used out of context. These can also be useful for variables, particularly in a long function, or function parameters if it's not obvious what to pass in.

In general, a TsDoc comment describes what the variable is, while a traditional comment describes what some logic does.

Example

/** Sets a firestore watch on an art piece */
const watchArtPiece = (
  /** An ArtPiece ID */
  id: string
) => {}

/** Holds the camera position to be used in a useFrame */
// Use a ref because this is updated on every frame
const cameraPosition = useRef(new Vector3());

Last updated