|
| 1 | +--- |
| 2 | +title: "Polymorphic Variant" |
| 3 | +description: "The Polymorphic Variant data structure in ReScript" |
| 4 | +canonical: "/docs/manual/latest/polymorphic-variant" |
| 5 | +--- |
| 6 | + |
| 7 | +# Polymorphic Variant |
| 8 | + |
| 9 | +Now that we know what [variant types](./variant) are, let's dive into a more specific version, so called polymorphic variants (or poly variants). |
| 10 | + |
| 11 | +First off, here are some key features: |
| 12 | + |
| 13 | +- Poly variants are **structurally typed** (in comparison to **nominally typed** variants). They can be used without an explicit type definition. |
| 14 | +- They allow easier JavaScript interop (compile to strings / objects with predictable `NAME` and `VAL` attribute) and don't need explicit runtime conversions, unlike common variants. |
| 15 | +- Due their structural nature, they oftentimes cause tricky type checking errors when types don't match up, which makes them a more advanced feature. |
| 16 | + |
| 17 | +## Basics |
| 18 | + |
| 19 | +This is how you'd define a poly variant type with an exact set of constructors: |
| 20 | + |
| 21 | +```res |
| 22 | +// Note the surrounding square brackets, and # for constructors |
| 23 | +type color = [ #Red | #Green | #Blue ] |
| 24 | +``` |
| 25 | + |
| 26 | +Here is how you'd construct a poly variant value: |
| 27 | + |
| 28 | +```res |
| 29 | +// This doesn't actually need any color type definition |
| 30 | +// beforehand |
| 31 | +let myColor = #Red |
| 32 | +``` |
| 33 | + |
| 34 | +We can also use poly variant types in annotations without an explicit type definition: |
| 35 | + |
| 36 | +```res |
| 37 | +let render = (color: [#Red | #Green | #Blue]) => { |
| 38 | + switch(color) { |
| 39 | + | _ => Js.log("...") |
| 40 | + } |
| 41 | +} |
| 42 | +
|
| 43 | +let color: [#Red] = #Red |
| 44 | +``` |
| 45 | + |
| 46 | +### Constructor Names |
| 47 | + |
| 48 | +Poly variant constructor names are less restrictive than in common variants (e.g. they don't need to be capitalized): |
| 49 | + |
| 50 | +```res |
| 51 | +type users = [ #admin | #moderator | #user ] |
| 52 | +
|
| 53 | +let admin = #admin |
| 54 | +``` |
| 55 | + |
| 56 | +In rare cases (mostly for JS interop reasons), it's also possible to define invalid identifiers, such as hypens or numbers: |
| 57 | + |
| 58 | +```res |
| 59 | +type numbers = [#\"1" | #\"2"] |
| 60 | +let one = #\"1" |
| 61 | +``` |
| 62 | + |
| 63 | +### Constructor Arguments |
| 64 | + |
| 65 | +This is equivalent to what we've already learned with common variants: |
| 66 | + |
| 67 | +```res |
| 68 | +type account = [ |
| 69 | + | #Anonymous |
| 70 | + | #Instagram(string) |
| 71 | + | #Facebook(string, int) |
| 72 | +] |
| 73 | +
|
| 74 | +let acc: account = #Instagram("test") |
| 75 | +``` |
| 76 | + |
| 77 | +### Annotations with Upper / Lower Bound Constraints |
| 78 | + |
| 79 | +There's also a way to define an "upper" and "lower" bound constraint for a poly variant type (that's why they are called _Polymorphic Variants_). Here is what it looks like in type annotations: |
| 80 | + |
| 81 | +```res |
| 82 | +// Only #Red allowed, no upper / lower bound (= exact) |
| 83 | +let basic: [#Red] = #Red |
| 84 | +
|
| 85 | +// May contain #Red, or any other value (open variant) |
| 86 | +// here, foreground will be an inferred type [> #Red | #Green] |
| 87 | +let foreground: [> #Red] = #Green |
| 88 | +
|
| 89 | +// The value must be "one of" #Red | #Blue |
| 90 | +// Only #Red and #Blue are valid values |
| 91 | +let background: [< #Red | #Blue] = #Red |
| 92 | +``` |
| 93 | + |
| 94 | +Don't worry about the upper / lower bound feature (aka polymorphism) just yet, since this is a very advanced topic that's often not really needed. For the sake of completeness, we mention a few details about it [later on](#lower--upper-bound-constraints). |
| 95 | + |
| 96 | + |
| 97 | +## Polymorphic Variants are Structurally Typed |
| 98 | + |
| 99 | +As we've already seen in the section above, poly variants are treated a little bit differently than common variants. Most notably, we don't need any explicit type definition to define a value. |
| 100 | + |
| 101 | +```res |
| 102 | +// inferred as [> #Red] |
| 103 | +let color = #Red |
| 104 | +``` |
| 105 | + |
| 106 | +The compiler will automatically infer the `color` binding as a value of type `[> #Red]`, which means `color` will type check with any other poly variant type that defines `#Red` in its constructors. |
| 107 | + |
| 108 | +This means that you can essentially mix and match poly variant values from different sources, as long as all constructors are defined in the final interface. For example: |
| 109 | + |
| 110 | +```res |
| 111 | +type rgb = [#Red | #Green | #Blue] |
| 112 | +
|
| 113 | +let colors: array<rgb> = [#Red] |
| 114 | +
|
| 115 | +// `other` is inferred as a type of array<[> #Green]> |
| 116 | +let other = [#Green] |
| 117 | +
|
| 118 | +// Because `other` is of type `array<[> Green]>`, |
| 119 | +// this will type check even though we didn't define |
| 120 | +// `other`to be of type rgb |
| 121 | +let all = Belt.Array.concat(colors, other) |
| 122 | +``` |
| 123 | + |
| 124 | +As you can see in the example above, the type checker doesn't really care that `color` is not annotated as an `array<rgb>` type. As soon as it hits the first constraint (`Belt.Array.concat`), it will try to check if `colors` and `other` unify into one polymorphic type. If there's a mismatch, you will get an error on the `Belt.Array.concat` call. |
| 125 | + |
| 126 | +**That means that it is very easy to get confusing type errors on the wrong locations!** |
| 127 | + |
| 128 | +For instance, if I'd make a typo like this: |
| 129 | + |
| 130 | +```res |
| 131 | +let other = [#GreeN] |
| 132 | +
|
| 133 | +let all = Belt.Array.concat(colors, other) |
| 134 | +``` |
| 135 | + |
| 136 | +I'd get an error on the `concat` call, even thought he error actually stems from my typo in my `other` definition. |
| 137 | + |
| 138 | +## JavaScript Output |
| 139 | + |
| 140 | +Poly variants are a [shared data structure](./shared-data-types), so they are very useful to bind to JavaScript. It is safe to rely on its compiled JS structure. |
| 141 | + |
| 142 | +A value compiles to the following JavaScript output: |
| 143 | + |
| 144 | +- If the variant value is a constructor without any payload, it compiles to a string of the same name |
| 145 | +- Values with a payload get compiled to an object with a `NAME` attribute stating the name of the constructor, and a `VAL` attribute containing the JS representation of the payload. |
| 146 | + |
| 147 | +Check the output in these examples: |
| 148 | + |
| 149 | + |
| 150 | +<CodeTab labels={["ReScript", "JS Output"]}> |
| 151 | + |
| 152 | +```res example |
| 153 | +let capitalized = #Hello |
| 154 | +let lowercased = #goodbye |
| 155 | +
|
| 156 | +let err = #error("oops!") |
| 157 | +
|
| 158 | +let num = #\"1" |
| 159 | +``` |
| 160 | + |
| 161 | +```js |
| 162 | +var capitalized = "Hello"; |
| 163 | + |
| 164 | +var lowercased = "goodbye"; |
| 165 | + |
| 166 | +var err = { |
| 167 | + NAME: "error", |
| 168 | + VAL: "oops!" |
| 169 | +}; |
| 170 | + |
| 171 | +var num = "1"; |
| 172 | +``` |
| 173 | + |
| 174 | +</CodeTab> |
| 175 | + |
| 176 | + |
| 177 | +**Note:** Poly variants play an important role for binding to JS functions in existing JavaScript. Check out the [Bind to JS Function page](bind-to-js-function#constrain-arguments-better) to learn more. |
| 178 | + |
| 179 | + |
| 180 | +### Bind to String Enums |
| 181 | + |
| 182 | +Let's assume we have a TypeScript module that expresses following (stringly typed) enum export: |
| 183 | + |
| 184 | +```js |
| 185 | +// direction.js |
| 186 | +enum Direction { |
| 187 | + Up = "UP", |
| 188 | + Down = "DOWN", |
| 189 | + Left = "LEFT", |
| 190 | + Right = "RIGHT", |
| 191 | +} |
| 192 | + |
| 193 | +export const myDirection = Direction.Up |
| 194 | +``` |
| 195 | + |
| 196 | +For this particular example, we can use poly variants to design the type for the imported `myDirection` value: |
| 197 | + |
| 198 | + |
| 199 | +<CodeTab labels={["ReScript", "JS Output"]}> |
| 200 | + |
| 201 | +```res |
| 202 | +type direction = [ #UP | #DOWN | #LEFT | #RIGHT ] |
| 203 | +@bs.module("./direction.js") external myDirection: direction = "myDirection" |
| 204 | +``` |
| 205 | + |
| 206 | +```js |
| 207 | +var DirectionJs = require("./direction.js"); |
| 208 | + |
| 209 | +var myDirection = DirectionJs.myDirection; |
| 210 | +``` |
| 211 | + |
| 212 | +</CodeTab> |
| 213 | + |
| 214 | +Since we were using poly variants, the JS Output is practically zero-cost and doesn't add any extra code! |
| 215 | + |
| 216 | +## Lower / Upper Bound Constraints |
| 217 | + |
| 218 | +There are a few different ways to define constraints on a poly variant type, such as `[>`, `[<` and `[]`. Some of them were briefly mentioned before, so in this section we will quickly explain what this syntax is about. |
| 219 | + |
| 220 | +**Note:** We added this info for educational purposes. In most cases you will not want to use any of this stuff, since it makes your APIs pretty unreadable / hard to use. |
| 221 | + |
| 222 | +### Exact (`[`) |
| 223 | + |
| 224 | +This is the simplest poly variant definition, and also the most practical one. Like a common variant type, this one defines an exact set of constructors. |
| 225 | + |
| 226 | +```res |
| 227 | +type rgb = [ #Red | #Green | #Blue ] |
| 228 | +
|
| 229 | +let color: rgb = #Green |
| 230 | +``` |
| 231 | + |
| 232 | +In the example above, `color` will only allow one of the three constructors that are defined in the `rgb` type. This is usually the way how poly variants should be defined. |
| 233 | + |
| 234 | +In case you want to define a type that is extensible in polymorphic ways (or in other words, subtyping allowed sets of constructors), you'll need to use the lower / upper bound syntax. |
| 235 | + |
| 236 | +### Lower Bound (`[>`) |
| 237 | + |
| 238 | +A lower bound defines the minimum set of constructors a poly variant type is aware of. It is also considered an "open poly variant type", because it doesn't restrict any additional values. |
| 239 | + |
| 240 | +Here is an example on how to make a minimum set of `basicBlueTones` extensible in a new `colors` type: |
| 241 | + |
| 242 | +```res |
| 243 | +type basicBlueTone<'a> = [> #Blue | #DeepBlue | #Azuro ] as 'a |
| 244 | +type color = basicBlueTone<[#Blue | #DeepBlue | #Azuro | #Purple]> |
| 245 | +
|
| 246 | +let color: color = #Purple |
| 247 | +
|
| 248 | +
|
| 249 | +// This will fail due to missing minimum constructors: |
| 250 | +type notWorking = basicBlueTone<[#Purple]> |
| 251 | +``` |
| 252 | + |
| 253 | +Here, the compiler will enforce the user to define `#Blue | #DeepBlue | #Azuro` as the minimum set of constructors when trying to extend `basicBlueTone<'a>`. |
| 254 | + |
| 255 | +### Upper Bound (`[<`) |
| 256 | + |
| 257 | +The upper bound works in the exact opposite way: The extending type may only use constructors that are stated in the lower bound constraing. |
| 258 | + |
| 259 | +Same example: |
| 260 | + |
| 261 | +```res |
| 262 | +type validRed<'a> = [< #Fire | #Crimson | #Ash] as 'a |
| 263 | +type myReds = validRed<[#Ash]> |
| 264 | +
|
| 265 | +// This will fail due to unlisted constructor not defined by the lower bound |
| 266 | +type notWorking = validRed<[#Purple]> |
| 267 | +``` |
| 268 | + |
| 269 | +## Tips & Tricks |
| 270 | + |
| 271 | +- In most scenarios, you should prefer common variants over polymorphic variants, since they offer better error messages and easier to spot errors in your program. |
| 272 | +- Polymorphic variants are pretty useful for doing zero-cost interop, e.g. when binding to JavaScript string enums, or to bind seemlessly between a tagged union type in TypeScript and ReScript |
| 273 | +- Even though we expanded a little bit on the upper / lower bounds / polymorphism, these examples only |
0 commit comments