title | description | canonical |
---|---|---|
Polymorphic Variant | The Polymorphic Variant data structure in ReScript | /docs/manual/v12.0.0/polymorphic-variant |
Polymorphic variants (or poly variant) are a cousin of variant. With these differences:
- They start with a
#
and the constructor name doesn't need to be capitalized. - They don't require an explicit type definition. The type is inferred from usage.
- Values of different poly variant types can share the constructors they have in common (aka, poly variants are "structurally" typed, as opposed to "nominally" typed).
They're a convenient and useful alternative to regular variants, but should not be abused. See the drawbacks at the end of this page.
We provide 3 syntaxes for a poly variant's constructor:
<CodeTab labels={["ReScript", "JS Output"]}>
letmyColor=#redletmyLabel=#"aria-hidden"letmyNumber=#7
varmyColor="red";varmyLabel="aria-hidden";varmyNumber=7;
Take a look at the output. Poly variants are great for JavaScript interop. For example, you can use it to model JavaScript string and number enums like TypeScript, but without confusing their accidental usage with regular strings and numbers.
myColor
uses the common syntax. The second and third syntaxes are to support expressing strings and numbers more conveniently. We allow the second one because otherwise it'd be invalid syntax since symbols like -
and others are usually reserved.
Although optional, you can still pre-declare a poly variant type:
// Note the surrounding square brackets, and # for constructorstypecolor= [#red | #green | #blue]
These types can also be inlined, unlike for regular variant:
<CodeTab labels={["ReScript", "JS Output"]}>
letrender= (myColor: [#red | #green | #blue]) => { switchmyColor { | #blue=>Console.log("Hello blue!") | #red | #green=>Console.log("Hello other colors") } }
functionrender(myColor){if(myColor==="green"||myColor==="red"){console.log("Hello other colors");}else{console.log("Hello blue!");}}
Note: because a poly variant value's type definition is inferred and not searched in the scope, the following snippet won't error:
<CodeTab labels={["ReScript", "JS Output"]}>
typecolor= [#red | #green | #blue] letrender=myColor=> { switchmyColor { | #blue=>Console.log("Hello blue!") | #green=>Console.log("Hello green!") // works! | #yellow=>Console.log("Hello yellow!") } }
functionrender(myColor){if(myColor==="yellow"){console.log("Hello yellow!");}elseif(myColor==="green"){console.log("Hello green!");}else{console.log("Hello blue!");}}
That myColor
parameter's type is inferred to be #red
, #green
or #yellow
, and is unrelated to the color
type. If you intended myColor
to be of type color
, annotate it as myColor: color
in any of the places.
This is similar to a regular variant's constructor arguments:
<CodeTab labels={["ReScript", "JS Output"]}>
typeaccount= [ | #Anonymous | #Instagram(string) | #Facebook(string, int) ] letme: account=#Instagram("Jenny") lethim: account=#Facebook("Josh", 26)
varme={NAME: "Instagram",VAL: "Jenny"};varhim={NAME: "Facebook",VAL: ["Josh",26]};
You can use poly variant types within other poly variant types to create a sum of all constructors:
<CodeTab labels={["ReScript", "JS Output"]}>
typered= [#Ruby | #Redwood | #Rust] typeblue= [#Sapphire | #Neon | #Navy] // Contains all constructors of red and blue.// Also adds #Papayawhiptypecolor= [red | blue | #Papayawhip] letmyColor: color=#Ruby
varmyColor="Ruby";
There's also some special pattern matching syntax to match on constructors defined in a specific poly variant type:
<CodeTab labels={["ReScript", "JS Output"]}>
// Continuing the previous example above...switchmyColor { | #...blue=>Console.log("This blue-ish") | #...red=>Console.log("This red-ish") | other=>Console.log2("Other color than red and blue: ", other) }
varother=myColor;if(other==="Neon"||other==="Navy"||other==="Sapphire"){console.log("This is blue-ish");}elseif(other==="Rust"||other==="Ruby"||other==="Redwood"){console.log("This is red-ish");}else{console.log("Other color than red and blue: ",other);}
This is a shorter version of:
switchmyColor { | #Sapphire | #Neon | #Navy=>Console.log("This is blue-ish") | #Ruby | #Redwood | #Rust=>Console.log("This is red-ish") | other=>Console.log2("Other color than red and blue: ", other) }
Since poly variants value don't have a source of truth for their type, you can write such code:
<CodeTab labels={["ReScript", "JS Output"]}>
typepreferredColors= [#white | #blue] letmyColor: preferredColors=#blueletdisplayColor=v=> { switchv { | #red=>"Hello red" | #green=>"Hello green" | #white=>"Hey white!" | #blue=>"Hey blue!" } } Console.log(displayColor(myColor))
varmyColor="blue";functiondisplayColor(v){if(v==="white"){return"Hey white!";}elseif(v==="red"){return"Hello red";}elseif(v==="green"){return"Hello green";}else{return"Hey blue!";}}console.log(displayColor("blue"));
With a regular variant, the line displayColor(myColor)
would fail, since it'd complain that the type of myColor
doesn't match the type of v
. No problem with poly variant.
Poly variants are great for JavaScript interop! You can share their values to JS code, or model incoming JS values as poly variants.
#red
and#"I am red 😃"
compile to JavaScipt"red"
and"I am red 😃"
.#1
compiles to JavaScript1
.- Poly variant constructor with 1 argument, like
Instagram("Jenny")
compile to a straightforward{NAME: "Instagram", VAL: "Jenny"}
. 2 or more arguments like#Facebook("Josh", 26)
compile to a similar object, but withVAL
being an array of the arguments.
For example, let's assume we want to bind to Intl.NumberFormat
and want to make sure that our users only pass valid locales, we could define an external binding like this:
<CodeTab labels={["ReScript", "JS Output"]}>
typet @scope("Intl") @valexternalmakeNumberFormat: ([#"de-DE" | #"en-GB" | #"en-US"]) =>t="NumberFormat"letintl=makeNumberFormat(#"de-DE")
varintl=Intl.NumberFormat("de-DE");
The JS output is identical to handwritten JS, but we also get to enjoy type errors if we accidentally write makeNumberFormat(#"de-DR")
.
More advanced usage examples for poly variant interop can be found in Bind to JS Function.
Let's assume we have a TypeScript module that expresses following enum export:
// direction.jsenumDirection{ Up ="UP", Down ="DOWN", Left ="LEFT", Right ="RIGHT",}exportconstmyDirection=Direction.Up
For this particular example, we can also inline poly variant type definitions to design the type for the imported myDirection
value:
<CodeTab labels={["ReScript", "JS Output"]}>
typedirection= [ #UP | #DOWN | #LEFT | #RIGHT ] @module("./direction.js") externalmyDirection: direction="myDirection"
varDirectionJs=require("./direction.js");varmyDirection=DirectionJs.myDirection;
Again: since we were using poly variants, the JS Output is practically zero-cost and doesn't add any extra code!
The previous poly variant type annotations we've looked at are the regular "closed" kind. However, there's a way to express "I want at least these constructors" (lower bound) and "I want at most these constructors" (upper bound):
// Only #Red allowed. Closed.letbasic: [#Red] =#Red// May contain #Red, or any other value. Open// here, foreground will actually be inferred as [> #Red | #Green]letforeground: [> #Red] =#Green// The value must be, at most, one of #Red or #Blue// Only #Red and #Blue are valid valuesletbackground: [< #Red | #Blue] =#Red
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.
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.
typergb= [ #Red | #Green | #Blue ] letcolor: rgb=#Green
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.
In case you want to define a type that is extensible, you'll need to use the lower / upper bound syntax.
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.
Here is an example on how to make a minimum set of basicBlueTones
extensible for a new color
type:
typebasicBlueTone<'a> = [> #Blue | #DeepBlue | #LightBlue ] as'atypecolor=basicBlueTone<[#Blue | #DeepBlue | #LightBlue | #Purple]> letcolor: color=#Purple// This will fail due to missing minimum constructors:typenotWorking=basicBlueTone<[#Purple]>
Here, the compiler will enforce the user to define #Blue | #DeepBlue | #LightBlue
as the minimum set of constructors when trying to extend basicBlueTone<'a>
.
Note: Since we want to define an extensible poly variant, we need to provide a type placeholder <'a>
, and also add as 'a
after the poly variant declaration, which essentially means: "Given type 'a
is constraint to the minimum set of constructors (#Blue | #DeepBlue | #LightBlue
) defined in basicBlueTone
".
The upper bound works in the opposite way than a lower bound: the extending type may only use constructors that are stated in the upper bound constraint.
Here another example, but with red colors:
typevalidRed<'a> = [< #Fire | #Crimson | #Ash] as'atypemyReds=validRed<[#Ash]> // This will fail due to unlisted constructor not defined by the lower boundtypenotWorking=validRed<[#Purple]>
You can convert a poly variant to a string
or int
at no cost:
<CodeTab labels={["ReScript", "JS Output"]}>
typecompany= [#Apple | #Facebook] lettheCompany: company=#Appleletmessage="Hello "++ (theCompany :> string)
vartheCompany="Apple";varmessage="Hello "+theCompany;
Note: for the coercion to work, the poly variant type needs to be closed; you'd need to annotate it, since otherwise, theCompany
would be inferred as [> #Apple]
.
One might think that polymorphic variants are superior to regular variants. As always, there are trade-offs:
Due to their "structural" nature, poly variant's type errors might be more confusing. If you accidentally write
#blur
instead of#blue
, ReScript will still error but can't indicate the correct source as easily. Regular variants' source of truth is the type definition, so the error can't go wrong.It's also harder to refactor poly variants. Consider this:
letmyFruit=#AppleletmySecondFruit=#AppleletmyCompany=#Apple
Refactoring the first one to
#Orange
doesn't mean we should refactor the third one. Therefore, the editor plugin can't touch the second one either. Regular variant doesn't have such problem, as these 2 values presumably come from different variant type definitions.You might lose some nice pattern match checks from the compiler:
letmyColor=#redswitchmyColor { | #red=>Console.log("Hello red!") | #blue=>Console.log("Hello blue!") }
Because there's no poly variant definition, it's hard to know whether the
#blue
case can be safely removed.
In most scenarios, we'd recommend to use regular variants over polymorphic variants, especially when you are writing plain ReScript code. In case you want to write zero-cost interop bindings or generate clean JS output, poly variants are oftentimes a better option.