Skip to content

Commit b7acc4f

Browse files
zthryyppy
andauthored
Record type spread and coercion blog post (#681)
* initial work on record type spread and coercion post * Update _blogposts/2023-05-17-enhancing-the-ergonomics-of-records.mdx * rewrite * Some polish / restructuring * Rename blog post file * Fix link --------- Co-authored-by: Patrick Ecker <[email protected]>
1 parent 8d792c4 commit b7acc4f

File tree

1 file changed

+186
-0
lines changed

1 file changed

+186
-0
lines changed
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
---
2+
author: rescript-team
3+
date: "2023-05-17"
4+
title: Enhanced Ergonomics for Record Types
5+
badge: roadmap
6+
description: |
7+
A tour of new capabilities coming to ReScript v11
8+
---
9+
10+
> This is the second post covering new capabilities that'll ship in ReScript v11. You can check out the first post on [better interop with customizable variants here](https://rescript-lang.org/blog/improving-interop).
11+
12+
[Records](https://rescript-lang.org/docs/manual/latest/record) are a fundamental part of ReScript, offering a clear and concise definition of complex data structures, immutability by default, great error messages, and support for exhaustive pattern matching.
13+
14+
Even though records are generally preferable for defining structured data, there are still a few ergonomic annoyances, such as...
15+
16+
1. Existing record types can't be extended, which makes them hard to compose
17+
2. Functions may only accept record arguments of the exact record type (no explicit sub-typing)
18+
19+
To mitigate the limitations above, one would need to retreat to [structural objects](https://rescript-lang.org/docs/manual/latest/object#sidebar) to allow more flexible object field sharing and sub-typing, at the cost of more complex type errors and no pattern matching capabilities.
20+
21+
We think that records are a much more powerful data structure though, so we want to encourage more record type usage for these scenarios. This is why ReScript v11 will come with two new big enhancements for record types: **Record Type Spread** and **Record Type Coercion**.
22+
23+
Let's dive right into the details and show-case the new language capabilities.
24+
25+
## Record Type Spread
26+
27+
As stated above, there was no way to share subsets of record fields with other record types. This means one had to copy / paste all the fields between the different record definitions. This was often tedious, error-prone and made code harder to maintain, especially when working with records with many fields.
28+
29+
In ReScript v11, you can now spread one or more record types into a new record type. It looks like this:
30+
31+
```rescript
32+
type a = {
33+
id: string,
34+
name: string,
35+
}
36+
37+
type b = {
38+
age: int
39+
}
40+
41+
type c = {
42+
...a,
43+
...b,
44+
active: bool
45+
}
46+
```
47+
48+
`type c` will now be:
49+
50+
```rescript
51+
type c = {
52+
id: string,
53+
name: string,
54+
age: int,
55+
active: bool,
56+
}
57+
```
58+
59+
Keeping it as straightforward as possible, type spreads are essentially a "copy-paste" operation for fields from one or more records to another, inlining the fields from the spread records into the new record. Please note: As for right now it is not possible to override fields in the target record type.
60+
61+
Needless to say, this feature offers a much better ergonomics when working with types with lots of fields, where variations of the same underlying type are needed.
62+
63+
### Use case: Extending the Built-in DOM Nodes
64+
65+
This feature can be particularly useful when extending DOM nodes. For instance, in the case of the animation library Framer Motion, one could easily extend the native DOM types with additional properties specific to the library, leading to a more seamless and type-safe integration.
66+
67+
This is how you could bind to a `div` in Framer Motion with the new record type spreads:
68+
69+
```rescript
70+
type animate = {} // definition omitted for brevity
71+
72+
type divProps = {
73+
// Note: JsxDOM.domProps is a built-in record type with all valid DOM node attributes
74+
...JsxDOM.domProps,
75+
initial?: animate,
76+
animate?: animate,
77+
whileHover?: animate,
78+
whileTap?: animate,
79+
}
80+
81+
module Div = {
82+
@module("framer-motion") external make: divProps => Jsx.element = "div"
83+
}
84+
```
85+
86+
You can now use `<Div />` as a `<motion.div />` component from Framer Motion and your type definition is quite simple and easy to maintain.
87+
88+
## Record Type Coercion
89+
90+
Record type coercion gives us more flexibility when passing around records in our application code. In other words, we can now coerce a record `a` to be treated as a record `b` at the type level, as long as the original record `a` contains the same set of fields in `b`. Here's an example:
91+
92+
```rescript
93+
type a = {
94+
name: string,
95+
age: int,
96+
}
97+
98+
type b = {
99+
name: string,
100+
age: int,
101+
}
102+
103+
let nameFromB = (b: b) => b.name
104+
105+
let a: a = {
106+
name: "Name",
107+
age: 35,
108+
}
109+
110+
let name = nameFromB(a :> b)
111+
```
112+
113+
Notice how we _coerced_ the value `a` to type `b` using the coercion operator `:>`. This works because they have the same record fields.
114+
115+
Additionally, we can also coerce records from `a` to `b` whenever `a` is a super-set of `b` (i.e. `a` containing all the fields of `b`, and more). The same example as above, slightly altered:
116+
117+
```rescript
118+
type a = {
119+
id: string,
120+
name: string,
121+
age: int,
122+
active: bool,
123+
}
124+
125+
type b = {
126+
name: string,
127+
age: int,
128+
}
129+
130+
let nameFromB = (b: b) => b.name
131+
132+
let a: a = {
133+
id: "1",
134+
name: "Name",
135+
age: 35,
136+
active: true,
137+
}
138+
139+
let name = nameFromB(a :> b)
140+
```
141+
142+
Notice how `a` now has more fields than `b`, but we can still coerce `a` to `b` because `b` has a subset of the fields of `a`.
143+
144+
In combination with [optional record fields](/docs/manual/latest/record#optional-record-fields), one may coerce a mandatory field of an `option` type to an optional field:
145+
146+
```rescript
147+
type a = {
148+
name: string,
149+
150+
// mandatory, but explicitly typed as option<int>
151+
age: option<int>,
152+
}
153+
154+
type b = {
155+
name: string,
156+
// optional field
157+
age?: int,
158+
}
159+
160+
let nameFromB = (b: b) => b.name
161+
162+
let a: a = {
163+
name: "Name",
164+
age: Some(35),
165+
}
166+
167+
let name = nameFromB(a :> b)
168+
```
169+
170+
The last example was rather advanced; the full feature set of record type coercion will later on be covered in a dedicated document page.
171+
172+
### Record Type Coercion is Explicit
173+
174+
Records are nominally typed, so it is not possible to pass a record `a` as record `b` without an explicit type coercion. This conscious design decision prevents accidental type matching on shapes rather than records, ensuring predictable and more robust type checking results.
175+
176+
## Try it out!
177+
178+
Feel free to check out the v11 alpha version on our [online playground](https://rescript-lang.org/try?version=v11.0.0-alpha.5&code=LYewJgrgNgpgBAJRgYxAJzAFQJ4AcYDKuaMAhmAKIAepwuscAvHAN4BQccALnvKU6w6c4AO1owAXHADOXNAEsRAcwA0QzqSWS4irms4BfNkJ744AIwHtho8VNkLl+4Zu27nRobC63gMAGJoIMAAQgIAFOZS5gCUTAB8FgB0Yn7GnN5wpFL8zNbCqdoARABy4kXOGlpSAMwArB7pcJmFAoWBwSHh4fwSibExbJ6gkAxIqBg4+ADCIDBoyPIgItS09PB5JrxZVuo6YPZyiqp7hYeOJzauUu57pMhc8gBu2uYgIFCNW2aWmzZnMiOTju1R0Ij0Qk8GRgPnaQVCESiFjijH6KXETUy2R2f2E8gOcCKAEYKqc7ISyn5SVdQfVKlkHs9tHIIDAvtDYeI2uIOqFur1+jFBkYgA), or install the alpha release via npm: `npm i [email protected]`.
179+
180+
This release is mainly for feedback purposes and not intended for production usage.
181+
182+
## Conclusion
183+
184+
The introduction of Record Type Spreads and Coercion in ReScript v11 will greatly improve the handling of record types. We're eager to see how you'll leverage these new language features in your ReScript projects.
185+
186+
Happy coding!

0 commit comments

Comments
 (0)