Alex Vipond
SHIFT + D

Web dev style guide

Published 2 months ago

I maintain a style guide for the way I write code 🤓

I follow my style guide rules extremely strictly in Baleada packages and in my personal projects, where I have free reign to enforce all my weirdest opinions. If you're out there source-diving my open source work, this style guide might explain some of the stuff you see.

This is a living document—it needs more work, and I tend to tweak rules, so I'll be back to edit in the future.

TypeScript rules

Variable naming

For the purposes of this section, Noun can be an actual noun, or an adjective that functions as a noun.

Variables named <verb>:

  • Are functions
  • Perform side effects, including parameter mutation in some cases
  • Internally might use reactivity APIs and/or perform side effects during a component lifecycle
  • Return nothing

Variables named to<Noun>:

  • Are functions
  • Transform something into something else of type <Noun>
  • Don't reach outside their scope for anything except functions that meet to<Noun> requirements, or global variables
  • Never have side effects
  • Never mutate any of their parameters
  • Can be named <noun>To<Noun> for clarity if necessary.
  • Would be named pipe<Noun>

Variables named narrow<Noun>:

  • Are functions
  • Are a subcategory of to<Noun> functions
  • Accept a parameter that could be one of multiple different types
  • Are type narrowers (they ensure that the parameter gets transformed to a specific type)

Variables named predicate<Noun>:

  • Are functions
  • Are a subcategory of to<Noun> functions
  • Accept a parameter that could be one of multiple different types
  • Are type predicates (they return a boolean indicating whether the parameter is a specific type)

Variables named create<FunctionName>:

  • Are functions
  • Are a subcategory of to<Noun> functions
  • Are higher order functions (they return a function)

Variables named preciselyCreate<FunctionName>:

  • Are a subcategory of create<FunctionName> functions
  • Return a create<FunctionName> function
  • Accept one optional object parameter whose key/value pairs fine-tune the behavior of the returned create<FunctionName> function

Variables named create<Noun>:

  • Are factory functions (they return an object)
  • Only include functions in the returned object

Variables named preciselyCreate<Noun>:

  • Are a subcategory of create<Noun> functions
  • Return a create<Noun> function
  • Accept one optional object parameter whose key/value pairs fine-tune the behavior of the returned create<Noun> function

Variables named create<Noun>Effects:

  • Are a subcategory of create<Noun> functions
  • Only include verb functions in the returned object

Variables named create<Noun>Transforms:

  • Are a subcategory of create<Noun> functions
  • Only include to<Noun> functions in the returned object

Variables named create<Noun>Fns:

  • Are a subcategory of create<Noun> functions
  • Include both verb and to<Noun> functions in the returned object

Variables named get<Noun>:

  • Are functions
  • Transform something into something else
  • Reach outside their scope to access state. this is considered outside of scope; constants are not.
  • May perform side effects

Variables named use<Noun>:

  • Are composables/hooks
  • Are factory functions
  • Returns an object with keys that are not created programmatically
  • Include reactive state in the returned object
  • Internally use reactivity APIs and/or perform side effects during a component lifecycle

Variables named <eventType>Effect:

  • Are functions
  • Perform side effects when eventType happens. eventType includes all event types, including custom event types like intersect or shiftCmdB, but also includes reactive variable names (the implication is that when the reactive variable changes, the effect will be performed)

Variables named <noun>By<OtherNoun>:

  • Are objects/maps/dictionaries that assign things as the values of keys that are otherThings

Variables named narrowed<Noun>:

  • Are the return value of a type narrower (named narrow<Noun>)

Variables named is<Noun>:

  • Are booleans
  • Are sometimes the return value of a type predicate (named predicate<Noun>)

Mutability

Immutability contributes to predictable systems. Prefer immutability.

When it's necessary and/or more readable to declare a variable with let and mutate it from inside if or switch statements, or for or while loops, prefer extracting that code to a separate function or IIFE that returns the let variable.

The major exception to this rule of course is reactive state, which is designed to be mutated, and comes with tools to help track and debug mutations, keeping the system predictable.

Conditionals

When handling booleans, prefer if statements, ternaries, and logical operators over switch statements.

Prefer switch statements for anything that is not a boolean.

Nested ternaries are fine if they're well formatted, but if statements with early returns are good for reduced indentation.

Avoid else. Prefer if statements with early returns.

if statements should always early return.

Function parameters

I need to make a decision tree for parameter design.

Factors to consider:

  • Unary functions are deliciously predictable and well-organized, but higher order functions breed unnecessarily complex variable names.
  • Passing multiple required parameters in a very specific order sucks. Passing multiple optional parameters in a very specific order sucks even more.
  • Multiple required parameters can be accepted in a single config or props object to keep the function unary, at the expense of requiring the user to explicitly name each required parameter (via object keys). It obeys the letter of the unary law, though not the spirit.
  • Options should always be accepted as a single optional options object parameter with default values, even if the object only accepts one option.

Pipelines

Prefer pipelines over method chaining, to maximize chances of tree-shaking.

If the piped value is an array at every stage of the pipeline, use the lazy-collections pipe function and iterator helpers.

Otherwise, use the Pipeable class from @baleada/logic.

Function organization

Functions should very roughly follow this distribution:

  • Nearly all functions should have fewer than 50 lines of logic
  • Roughly 30% of functions should have fewer than 10 lines of logic
  • When any block of tightly coupled UI logic exceeds 7 lines, it should almost always be extracted into a one-off, non-reusable function

When defining a tree of functions, write it in depth-first order. In other words,

  • Write the top-level function first
  • If the top-level function calls a nested function or references a nested constant ("child"), define that nested function or constant next
  • If the nested function calls another nested function ("grandchild"), define that next
  • Once all grandchildren are defined, define the next child function
  • Once all children are defined, define the next top-level function

Since JS constants execute immediately and don't allow you to reference a constant before it's defined, this style must be changed for nested constants. Nested constants should be defined in reverse-depth-first order. In other words, write the grandchildren constants first, then the children constants, then the top-level constants.

Type organization

When defining a tree of types, write it in depth-first order. In other words,

  • Write the top-level type first
  • If the top-level type calls a nested type or references a nested constant ("child"), define that nested type or constant next
  • If the nested type calls another nested type ("grandchild"), define that next
  • Once all grandchildren are defined, define the next child type
  • Once all children are defined, define the next top-level type

Vue rules

All TypeScript rules also apply to Vue code.

Component categories

All components fit into the following categories, sorted here from least to most complex markup structure:

Category
Description
Brand
Decorative markup, including marketing materials and logos. Product-specific, not suitable for publishing in a separate package.
Control
Controlled components that are abstractions over individual form controls. Ideally, these should be functional components that accept state from function ref composables via props. Suitable for publishing in a package to use across a family of products.
Form
Combinations of controls. Usually app-specific, but in some cases can be suitable for publishing in a package to use across a family of products.
??? (WIP)
Combinations of forms and/or static markup. Some ??? components might be appropriate to publish in a separate package to use across a family of products.
Page
Components that Vue Router can navigate to.
Layout
Parent components for pages. Passed to the top-level component property of a route.

Component names should start with their category:

  • BrandLogo
  • ControlTextbox
  • FormAccountSettings
  • etc.

Component code organization

Code should very roughly follow this distribution:

  • Nearly all components should have fewer than 50 lines of UI logic, i.e. code in script setup
  • Roughly 30% of components should have fewer than 10 lines of UI logic
  • Nearly all components should have fewer than 100 lines of template code
  • Roughly 30% of components should have fewer than 30 lines of template code
  • When any block of tightly coupled UI logic exceeds 7 lines, it should almost always be extracted into a one-off, non-reusable composable
  • When any block of tightly coupled template code exceeds 50 lines, it should almost always be extracted into a one-off, non-reusable component

React rules

All TypeScript rules also apply to React code.

Component code organization

Code should very roughly follow this distribution:

  • Nearly all components should have fewer than 50 lines of UI logic, i.e. code before returning JSX
  • Roughly 30% of components should have fewer than 10 lines of UI logic
  • Nearly all components should have fewer than 100 lines of JSX
  • Roughly 30% of components should have fewer than 30 lines of JSX
  • When any block of tightly coupled UI logic exceeds 7 lines, it should almost always be extracted into a one-off, non-reusable hook
  • When any block of tightly coupled JSX exceeds 50 lines, it should almost always be extracted into a one-off, non-reusable component

CSS rules

  • Use Tailwind
  • Use Tailwind
  • Use Tailwind

Tailwind class order:

  1. Any classes that primarily affect siblings' layout
  2. Any classes that primarily affect children's layout
  3. Any stylistic classes, affecting the element or its children, as long as the classes won't affect layout. I break this category into as many lines as I need, if it's easier to read.

Prefer adding gap to a parent element rather than margin to a child element. Whitespace is a concern of parent elements, not their children.

ON THIS PAGE

Web dev style guideTypeScript rulesVariable namingMutabilityConditionalsFunction parametersPipelinesFunction organizationType organizationVue rulesComponent categoriesComponent code organizationReact rulesComponent code organizationCSS rules