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:
- All 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
MergeBranchyou would createtype branchName = stringand 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 and to help the compiler to produce better error messages during refactoring.
- 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
refkeyword) should be used only with a very strong reason. - Default to using non polymorphic variants (
type rgb = Red | Green | Blueovertype rgb = [#red | #green | #blue]). We think of polymorphic variants as a lesser-typed approach and prefer to have stronger types. - Usage of polymorphic is not discouraged. However have in mind that they should always be used strictly typed, if possible. They can be utilised to various benefits, such as
- Directly mapping to strings in JS (
#myStringin res ->"myString"in JS or even#"my string" in res ->"my string"` in JS) - Powerful, categorised switching (
#...partialType => doSomething()) when used with combined types (See docs) - In regards to structural sharing (See docs) where the fact that polymorphic variants don't have a source of truth for their type. Beware that this is just as much a con as it can be a pro and use with care.
-
Polymorphic variants should be camelCase.
#successrather than#Success. -
Non polymorphic variants should be PascalCase.
MergeBranchrather thanmergeBranch. -
Encapsulate internal code with the
$$private(...)block, so the public interface of a module is clear and the internal code is not leaked into the public scope. -
Prefer using uncurried functions over curried (i. e.
reduceUoverreducein 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 in runtime. -
Put multiline styles in the top of the file.
-
When deciding between
thing->function->functionandfunction(thing)->functionread 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-jsonlibJson.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.Array2andJs.String2. NEVER useJs.ArrayandJs.String. This is required to keep the->piping consistent. A nice suggestion of how to achieve that can be found here #2 -
Use
Lists for resursive data structures. UseArrayotherwise. I.e.
let rec len = (myList: list('a)) =>
switch myList {
| [] => 0
| [_, ...tail] => 1 + len(tail)
}
- Do not overuse reduce. Consider other options, like
mapandflatMapto achieve the result. Prefer reduce overrefthough. - Open
Beltglobally. It saves a lot of typing. - Prefer
Belt.Resultover 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. - If you want to use
openlocally, use it inside a function.
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.