At Avo we use Rescript as our primary programming language. To allow the development team to deliver better code and reviews, we've come up with a set of guidelines:
-
File names, just like modules, should be in PascalCase:
EventsList.res
-
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)
-
Define type signatures explicitly unless you're sure that they should not be defined. 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.
-
Use named parameters if a function has multiple parameters of same type. This will prevent accidentally mixing up the parameters when calling the function.
-
Tend to use named 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. -
Polymorphic variants should be lowercase.
#success
rather than#Success
-
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 performs better on runtime. -
Put multiline styles in the top of the file.
-
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")
-
When working with JSON create decoders and encoders with the
bs-json
libJson.Decode.(field("token", string, "myToken")) Json.Encode.(object_([ ("token", string("myToken")), ("returnSecureToken", bool(true)), ])
see more about using variants and this blog post for more examples.
We tried other approaches but they all tend to end up with similar amount of work and less constrol.
-
When defining functions keep the types definition in the function, not in the variable, to allow more freedom for type inferrence, i.e.
let a = (b: string, c: int): string => b === "ok"
overlet a: ((string, int) => boolean) = (b, c) => b === "ok"
-
ALWAYS use
Js.Array2
andJs.String2
. NEVER useJs.Array
andJs.String
. This is required to keep the->
piping consistent. A nice suggestion of how to achieve that can be found here #2 -
Use
List
s for resursive data structures. UseArray
otherwise. I.e.
let rec len = (myList: list('a)) =>
switch myList {
| [] => 0
| [_, ...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. It saves a lot of typing. - Prefer
Belt.Result
over throwing exceptions. This would make the execution flow more homogeneous. Exceptions are generally considered to be avoided nowadays. - Don't put more than 3 React components in a single file. Use separate files for big components or components that are used in multiple places.
- Use
rescript-promise
(Promise.then(…)
) overJs.Promise
. The bindings are nicer, have stricter error handling and are recommended as of Rescript 10.1.
When creating cloud functions, only define a single function in a file, defined as let handle = {...}
In Index.re
(which relates to index.js
required by google cloud functions)
refer to the function with
[Type of Trigger][Name of Function] = File.handle
Types of Triggers
- trigger (Firestore document trigger)
- https
- pubsub
Example:
let httpsGetCustomers = GetCustomersEndpoint.handle
let triggerOnCustomerAdded = OnCustomerAdded.handle
let pubsubDeleteUserData = DeleteUserData.handle
The name defined here will be the name of the cloud function itself and helps us to easily map code with function for debugging and logging.