- Define meaningful types for every variant and polymorphic variant you introduce. For example, when defining
MergeBranch
you would createtype branchName = string;
and the definition would look like:MergeBranch(branchName)
instead ofMergeBranch(string)
. This encodes more meaning into the code, the editor plugin will still say it is astring
ultimately. - In general it is better to type function signatures more, rather than less. Even when the compiler can infer the type it still is often useful to state it explicitly for readability of the code, especially outside of the IDE (reviewing PRs for example). If type names are too long (eg with graphql types), you can create type aliases - eg.
type shortTypeName = Complicated.Long.Nested.type_name_that_makes_the_code_look_messy
. - Use labelled parameters if a function has multiple parameters of same type. This will prevent accidentally mixing up the parameters when calling the function. Eg don't do
let transfer: (address, address) => Promise.t<tx>
, but ratherlet transfer: (~tokenAddress=address, ~recipient=address) => Promise.t<tx>
- Tend to use labelled parameters more often. This is advised for readability.
- Imperative code (i.e. the
ref
keyword) should be used only with a very strong reason. - Default to using non polymorphic variants (
type rgb = Red | Green | Blue;
overtype rgb = [#Red | #Green | #Blue];
). We think of polymorphic variants as a lesser-typed approach and prefer to have stronger typing. Promenant exception is for string interop or coercion. Another exception is for more flexibility or if you want other kinds of constraints. - All new components should be Jsx3 components. This is done to be able to use the latest language features.
- When deciding between
thing->function->function
andfunction(thing)->function
read it out and pick the one that makes more sense in a natural language, i.e.Firebase.firestore(firebase)->Firebase.root("main")
andmaybeSomething->Belt.Option.getWithDefault("definitelySomething")
- Liberally simplify functions from other modules at the top of your code. Eg prefer
a->mul(b)
toa->Ethers.BigNumber.mul(b)
by declaring itlet {mul} = module(Ethers.BigNumber)
. Re-name them to avoid collisions. - When defining functions prefer to keep the types definition in the function, not in the variable, to allow more freedom for type inference, i.e.
let a = (b: string, c: int): bool => b === "ok"
overlet a: ((string, int) => boolean) = (b, c) => b === "ok"
. This means parts of the type can be left out to be inferred rather and makes the code feel less rigid. - ALWAYS use
Js.Array2
andJs.String2
. NEVER useJs.Array
andJs.String
. This is required to keep the->
piping consistent. Prefer usingBelt.Array
if possible too. - Basically only use
List
s for recursive data structures (better pattern matching in switch statements, more efficient etc.) or where immutability is crucial. UseArray
otherwise. I.e.
let rec len = (myList: list<'a>) =>
switch myList {
| list{} => 0
| list{_, ...tail} => 1 + len(tail)
};
- Do not overuse reduce. Consider other options, like
map
andflatMap
to achieve the result. Prefer reduce overref
though. - Open
Belt
globally. Makes code look cleaner. (Should already be setup in the codebase). - Prefer
Belt.Result
over throwing exceptions. This would make the execution flow more homogeneous. Exceptions are generally considered to be avoided nowadays.
- Prefer proper bindings over
%raw
. - Where possible type as much as possible.
- When you use template types, rather write out what they are to be descriptive (eg. rather than
`a
do`configObject
). - For config that is static, it is sufficient to type things and not do runtime verification of the types (eg with a library like decco or bs-json) - so no need to make EVERYTHING an 'option'. For dynamic data from external APIs, rather use a decoder or at least make types optional for runtime validation of data.
- Prefer using uncurried functions over curried (i. e.
reduceU
overreduce
in Belt.List*)*, it will help the compiler to produce cleaner error messages in cases when there is a parameters mismatch, which happens quite often during refactoring. Another benefit is that it works faster.