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
andto<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 likeintersect
orshiftCmdB
, 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
thing
s as the values of keys that areotherThing
s
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
orprops
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:
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:
- Any classes that primarily affect siblings' layout
- Any classes that primarily affect children's layout
- 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.