Record
Records are like JavaScript objects but:
are immutable by default
have fixed fields (not extensible)
Type Declaration
A record needs a mandatory type declaration:
Creation
To create a person
record (declared above):
When you create a new record value, ReScript tries to find a record type declaration that conforms to the shape of the value. So the me
value here is inferred as of type person
.
The type is found by looking above the me
value. Note: if the type instead resides in another file or module, you need to explicitly indicate which file or module it is:
Either of the above 3 says "this record's definition is found in the School file". The first one, the regular type annotation, is preferred.
Access
Use the familiar dot notation:
Immutable Update
New records can be created from old records with the ...
spread operator. The original record isn't mutated.
Note: spread cannot add new fields to the record value, as a record's shape is fixed by its type.
Mutable Update
Record fields can optionally be mutable. This allows you to efficiently update those fields in-place with the =
operator.
Fields not marked with mutable
in the type declaration cannot be mutated.
JavaScript Output
ReScript records compile to straightforward JavaScript objects; see the various JS output tabs above.
Optional Record Fields
ReScript v10
introduced optional record fields. This means that you can define fields that can be omitted when creating the record. It looks like this:
Notice how name
has a suffixed ?
. That means that the field itself is optional.
Creation
You can omit any optional fields when creating a record. Not setting an optional field will default the field's value to None
:
This has consequences for pattern matching, which we'll expand a bit on soon.
Immutable Update
Updating an optional field via an immutable update above lets you set that field value without needing to care whether it's optional or not.
However, if you want to set the field to an optional value, you prefix that value with ?
:
You can unset an optional field's value via that same mechanism by setting it to ?None
.
Pattern Matching on Optional Fields
Pattern matching, one of ReScript's most important features, has two caveats when you deal with optional fields.
When matching on the value directly, it's an option
. Example:
But, when matching on the field as part of the general record structure, it's treated as the underlying, non-optional value:
Sometimes you do want to know whether the field was set or not. You can tell the pattern matching engine about that by prefixing your option match with ?
, like this:
Tips & Tricks
Record Types Are Found By Field Name
With records, you cannot say "I'd like this function to take any record type, as long as they have the field age
". The following won't work as intended:
Instead, getAge
will infer that the parameter entity
must be of type monster
, the closest record type with the field age
. The following code's last line fails:
RESlet kraken = {age: 9999, hasTentacles: true}
let me = {age: 5, name: "Baby ReScript"}
getAge(kraken)
getAge(me) // type error!
The type system will complain that me
is a person
, and that getAge
only works on monster
. If you need such capability, use ReScript objects, described here.
Optional Fields in Records Can Be Useful for Bindings
Many JavaScript APIs tend to have large configuration objects that can be a bit annoying to model as records, since you previously always needed to specify all record fields when creating a record.
Optional record fields, introduced in v10
, is intended to help with this. Optional fields will let you avoid having to specify all fields, and let you just specify the one's you care about. A significant improvement in ergonomics for bindings and other APIs with for example large configuration objects.
Design Decisions
After reading the constraints in the previous sections, and if you're coming from a dynamic language background, you might be wondering why one would bother with record in the first place instead of straight using object, since the former needs explicit typing and doesn't allow different records with the same field name to be passed to the same function, etc.
The truth is that most of the times in your app, your data's shape is actually fixed, and if it's not, it can potentially be better represented as a combination of variant (introduced next) + record instead.
Since a record type is resolved through finding that single explicit type declaration (we call this "nominal typing"), the type error messages end up better than the counterpart ("structural typing", like for tuples). This makes refactoring easier; changing a record type's fields naturally allows the compiler to know that it's still the same record, just misused in some places. Otherwise, under structural typing, it might get hard to tell whether the definition site or the usage site is wrong.