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
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:
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:
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
Last updated