You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Copy file name to clipboardExpand all lines: pages/docs/manual/latest/variant.mdx
+198-34
Original file line number
Diff line number
Diff line change
@@ -152,6 +152,26 @@ var me = {
152
152
153
153
The output is slightly uglier and less performant than the former.
154
154
155
+
## Variant Type Spreads
156
+
Just like [with records](record#record-type-spread), it's possible to use type spreads to create new variants from other variants:
157
+
158
+
```rescript
159
+
type a = One | Two | Three
160
+
type b = | ...a | Four | Five
161
+
```
162
+
163
+
Type `b` is now:
164
+
```rescript
165
+
type b = One | Two | Three | Four | Five
166
+
```
167
+
168
+
Type spreads act as a 'copy-paste', meaning all constructors are copied as-is from `a` to `b`. Here are the rules for spreads to work:
169
+
- You can't overwrite constructors, so the same constructor name can exist in only one place as you spread. This is true even if the constructors are identical.
170
+
- All variants and constructors must share the same runtime configuration - `@unboxed`, `@tag`, `@as` and so on.
171
+
- You can't spread types in recursive definitions.
172
+
173
+
Note that you need a leading `|` if you want to use a spread in the first position of a variant definition.
174
+
155
175
### Pattern Matching On Variant
156
176
157
177
See the [Pattern Matching/Destructuring](pattern-matching-destructuring) section later.
@@ -160,10 +180,9 @@ See the [Pattern Matching/Destructuring](pattern-matching-destructuring) section
160
180
161
181
A variant value compiles to 3 possible JavaScript outputs depending on its type declaration:
162
182
163
-
- If the variant value is a constructor with no payload, it compiles to a string.
164
-
- If it's a constructor with a payload, it compiles to an object with the field `TAG` and the field `_0` for the first payload, `_1` for the second payload, etc.
165
-
- An exception to the above is a variant whose type declaration contains only a single constructor with payload. In that case, the constructor compiles to an object without the `TAG` field.
166
-
- Labeled variant payloads (the inline record trick earlier) compile to an object with the label names instead of `_0`, `_1`, etc. The object might or might not have the `TAG` field as per previous rule.
183
+
- If the variant value is a constructor with no payload, it compiles to a string of the constructor name. Example: `Yes` compiles to `"Yes"`.
184
+
- If it's a constructor with a payload, it compiles to an object with the field `TAG` and the field `_0` for the first payload, `_1` for the second payload, etc. The value of `TAG` is the constructor name as string by default, but note that the name of the `TAG` field as well as the string value used for each constructor name [can be customized](#tagged-variants).
185
+
- Labeled variant payloads (the inline record trick earlier) compile to an object with the label names instead of `_0`, `_1`, etc. The object will have the `TAG` field as per the previous rule.
167
186
168
187
Check the output in these examples:
169
188
@@ -294,7 +313,7 @@ Now, this maps 100% to the TypeScript code, including letting us bring over the
294
313
295
314
### String literals
296
315
297
-
The same logic is easily applied to string literals from TypeScript, only here the benefit is even larger, because string literals have the same limitations in TypeScript that polymorphic variants have in ReScript.
316
+
The same logic is easily applied to string literals from TypeScript, only here the benefit is even larger, because string literals have the same limitations in TypeScript that polymorphic variants have in ReScript:
298
317
299
318
```typescript
300
319
// direction.ts
@@ -303,9 +322,18 @@ type direction = "UP" | "DOWN" | "LEFT" | "RIGHT";
303
322
304
323
There's no way to attach documentation strings to string literals in TypeScript, and you only get the actual value to interact with.
305
324
325
+
### Valid `@as` payloads
326
+
Here's a list of everything you can put in the `@as` tag of a variant constructor:
327
+
- A string literal: `@as("success")`
328
+
- An int: `@as(5)`
329
+
- A float: `@as(1.5)`
330
+
- True/false: `@as(true)` and `@as(false)`
331
+
- Null: `@as(null)`
332
+
- Undefined: `@as(undefined)`
333
+
306
334
## Untagged variants
307
335
308
-
With _untagged variants_ it is possible to represent a heterogenous array.
336
+
With _untagged variants_ it is possible to mix types together that normally can't be mixed in the ReScript type system, as long as there's a way to discriminate them at runtime. For example, with untagged variants you can represent a heterogenous array:
309
337
310
338
```rescript
311
339
@unboxed type listItemValue = String(string) | Boolean(bool) | Number(float)
In the above example, reaching back into the values is as simple as pattern matching on them.
325
353
326
-
### Pattern matching on nullable values
354
+
### Advanced: Unboxing rules
355
+
#### No overlap in constructors
356
+
A variant can be unboxed if no constructors have overlap in their runtime representation.
357
+
358
+
For example, you can't have `String1(string) | String2(string)` in the same unboxed variant, because there's no way for ReScript to know at runtime which of `String1` or `String2` that `string` belongs to, as it could belong to both.
359
+
The same goes for two records - even if they have fully different shapes, they're still JavaScript `object` at runtime.
360
+
361
+
Don't worry - the compiler will guide you and ensure there's no overlap.
362
+
363
+
#### What you can unbox
364
+
Here's a list of all possible things you can unbox:
365
+
-`string`: `String(string)`
366
+
-`float`: `Number(float)`. Notice `int` cannot be unboxed, because JavaScript only has `number` (not actually `int` and `float` like in ReScript) so we can't disambiguate between `float` and `int` at runtime.
367
+
-`bool`: `Boolean(bool)`
368
+
-`array<'value>`: `List(array<string>)`
369
+
-`promise<'value>`: `Promise(promise<string>)`
370
+
-`Dict.t`: `Object(Dict.t<string>)`
371
+
-`Date.t`: `Date(Date.t)`. A JavaScript date.
372
+
-`Blob.t`: `Blob(Blob.t)`. A JavaScript blob.
373
+
-`File.t`: `File(File.t)`. A JavaScript file.
374
+
-`RegExp.t`: `RegExp(RegExp.t)`. A JavaScript regexp instance.
375
+
376
+
Again notice that the constructor names can be anything, what matters is what's in the payload.
377
+
378
+
> **Under the hood**: Untagged variants uses a combination of JavaScript `typeof` and `instanceof` checks to discern between unboxed constructors at runtime. This means that we could add more things to the list above detailing what can be unboxed, if there are useful enough use cases.
379
+
380
+
### Pattern matching on unboxed variants
381
+
Pattern matching works the same on unboxed variants as it does on regular variants. In fact, in the perspective of ReScript's type system there's no difference between untagged and tagged variants. You can do virtually the same things with both. That's the beauty of untagged variants - they're just variants to you as a developer.
382
+
383
+
Here's an example of pattern matching on an unboxed nullable value that illustrates the above:
327
384
328
385
```rescript
329
-
// The type definition below is inlined here to examplify, but this definition will live in [Core](https://github.com/rescript-association/rescript-core) and be easily accessible
330
386
module Null = {
331
387
@unboxed type t<'a> = Present('a) | @as(null) Null
332
388
}
@@ -345,12 +401,13 @@ let getBestFriendsAge = user =>
345
401
| _ => None
346
402
}
347
403
```
404
+
No difference to how you'd do with a regular variant. But, the runtime representation is different to a regular variant.
348
405
349
406
> Notice how `@as` allows us to say that an untagged variant case should map to a specific underlying _primitive_. `Present` has a type variable, so it can hold any type. And since it's an unboxed type, only the payloads `'a` or `null` will be kept at runtime. That's where the magic comes from.
350
407
351
408
### Decoding and encoding JSON idiomatically
352
409
353
-
With untagged variants, we have everything we need to define a JSON type:
410
+
With untagged variants, we have everything we need to define a native JSON type:
354
411
355
412
```rescript
356
413
@unboxed
@@ -370,9 +427,8 @@ Here's an example of how you could write your own JSON decoders easily using the
370
427
```rescript
371
428
@unboxed
372
429
type rec json =
373
-
| @as(false) False
374
-
| @as(true) True
375
430
| @as(null) Null
431
+
| Boolean(bool)
376
432
| String(string)
377
433
| Number(float)
378
434
| Object(Js.Dict.t<json>)
@@ -432,43 +488,151 @@ let usersToJson = users => Array(users->Array.map(userToJson))
432
488
433
489
This can be extrapolated to many more cases.
434
490
435
-
// ### Unboxable types
491
+
### Advanced: Catch-all Constructors
492
+
With untagged variants comes a rather interesting capability - catch-all cases are now possible to encode directly into a variant.
493
+
494
+
Let's look at how it works. Imagine you're using a third party API that returns a list of available animals. You could of course model it as a regular `string`, but given that variants can be used as "typed strings", using a variant would give you much more benefit:
436
495
437
-
// TODO #734: Add a list of what can currently be unboxed (and why), and a note that it's possible that more things could be unboxed in the future.
496
+
<CodeTablabels={["ReScript", "JS Output"]}>
497
+
```rescript
498
+
type animal = Dog | Cat | Bird
438
499
439
-
// ### Catch all
500
+
type apiResponse = {
501
+
animal: animal
502
+
}
440
503
441
-
// TODO #733: Add a small section on the "catch all" trick, and what kind of things that enable.
504
+
let greetAnimal = (animal: animal) =>
505
+
switch animal {
506
+
| Dog => "Wof"
507
+
| Cat => "Meow"
508
+
| Bird => "Kashiiin"
509
+
}
510
+
```
511
+
```javascript
512
+
```
513
+
</CodeTab>
442
514
443
-
// ## Variant spread
444
515
445
-
// TODO #732
516
+
This is all fine and good as long as the API returns `"Dog"`, `"Cat"` or `"Bird"` for `animal`.
517
+
However, what if the API changes before you have a chance to deploy new code, and can now return `"Turtle"` as well? Your code would break down because the variant `animal` doesn't cover `"Turtle"`.
446
518
447
-
## Coercion
519
+
So, we'll need to go back to `string`, loosing all of the goodies of using a variant, and then do manual conversion into the `animal` variant from `string`, right?
520
+
Well, this used to be the case before, but not anymore! We can leverage untagged variants to bake in handling of unknown values into the variant itself.
448
521
449
-
You can convert a variant to a `string` or `int` at no cost:
522
+
Let's update our type definition first:
523
+
```rescript
524
+
@unboxed
525
+
type animal = Dog | Cat | Bird | UnknownAnimal(string)
526
+
```
527
+
528
+
Notice we've added `@unboxed` and the constructor `UnknownAnimal(string)`. Remember how untagged variants work? You remove the constructors and just leave the payloads. This means that the variant above at runtime translates to this (made up) JavaScript type:
529
+
```
530
+
type animal = "Dog" | "Cat" | "Bird" | string
531
+
```
532
+
So, any string not mapping directly to one of the payloadless constructors will now map to the general `string` case.
533
+
534
+
As soon as we've added this, the compiler complains that we now need to handle this additional case in our pattern match as well. Let's fix that:
450
535
451
536
<CodeTablabels={["ReScript", "JS Output"]}>
537
+
```rescript
538
+
@unboxed
539
+
type animal = Dog | Cat | Bird | UnknownAnimal(string)
452
540
453
-
```res
454
-
type company = Apple | Facebook | Other(string)
455
-
let theCompany: company = Apple
541
+
type apiResponse = {
542
+
animal: animal
543
+
}
456
544
457
-
let message = "Hello " ++ (theCompany :> string)
545
+
let greetAnimal = (animal: animal) =>
546
+
switch animal {
547
+
| Dog => "Wof"
548
+
| Cat => "Meow"
549
+
| Bird => "Kashiiin"
550
+
| UnknownAnimal(otherAnimal) =>
551
+
`I don't know how to greet animal ${otherAnimal}`
552
+
}
553
+
```
554
+
```javascript
555
+
functiongreetAnimal(animal) {
556
+
if (!(animal ==="Cat"|| animal ==="Dog"|| animal ==="Bird")) {
557
+
return"I don't know how to greet animal "+ animal;
558
+
}
559
+
switch (animal) {
560
+
case"Dog" :
561
+
return"Wof";
562
+
case"Cat" :
563
+
return"Meow";
564
+
case"Bird" :
565
+
return"Kashiiin";
566
+
567
+
}
568
+
}
458
569
```
570
+
</CodeTab>
459
571
460
-
```js
461
-
var theCompany ="Apple";
462
-
var message ="Hello "+ theCompany;
572
+
There! Now the external API can change as much as it wants, we'll be forced to write all code that interfaces with `animal` in a safe way that handles all possible cases. All of this baked into the variant definition itself, so no need for labor intensive manual conversion.
573
+
574
+
This is useful in any scenario when you use something enum-style that's external and might change. Additionally, it's also useful when something external has a large number of possible values that are known, but where you only care about a subset of them. With a catch-all case you don't need to bind to all of them just because they can happen, you can safely just bind to the ones you care about and let the catch-all case handle the rest.
575
+
576
+
## Coercion
577
+
In certain situations, variants can be coerced to other variants, or to and from primitives. Coercion is always zero cost.
578
+
579
+
### Coercing Variants to Other Variants
580
+
You can coerce a variant to another variant if they're identical in runtime representation, and additionally if the variant you're coercing can be represented as the variant you're coercing to.
581
+
582
+
Here's an example using [variant type spreads](#variant-type-spreads):
583
+
```rescript
584
+
type a = One | Two | Three
585
+
type b = | ...a | Four | Five
586
+
587
+
let one: a = One
588
+
let four: b = Four
589
+
590
+
// This works because type `b` can always represent type `a` since all of type `a`'s constructors are spread into type `b`
591
+
let oneAsTypeB = (one :> b)
463
592
```
464
593
465
-
</CodeTab>
594
+
### Coercing Variants to Primitives
595
+
Variants that are guaranteed to always be represented by a single primitive at runtime can be coerced to that primitive.
596
+
597
+
It works with strings, the default runtime representation of payloadless constructors:
598
+
```rescript
599
+
// Constructors without payloads are represented as `string` by default
600
+
type a = One | Two | Three
601
+
602
+
let one: a = One
603
+
604
+
// All constructors are strings at runtime, so you can safely coerce it to a string
605
+
let oneAsString = (one :> string)
606
+
```
607
+
608
+
If you were to configure all of your construtors to be represented as `int` or `float`, you could coerce to those too:
609
+
```rescript
610
+
type asInt = | @as(1) One | @as(2) Two | @as(3) Three
611
+
612
+
let oneInt: asInt = One
613
+
let toInt = (oneInt :> int)
614
+
```
615
+
616
+
### Advanced: Coercing `string` to Variant
617
+
In certain situtations it's possible to coerce a `string` to a variant. This is an advanced technique that you're unlikely to need much, but when you do it's really useful.
618
+
619
+
You can coerce a `string` to a variant when:
620
+
- Your variant is `@unboxed`
621
+
- Your variant has a "catch-all" `string` case
622
+
623
+
Let's look at an example:
624
+
```rescript
625
+
@unboxed
626
+
type myEnum = One | Two | Other(string)
627
+
628
+
// Other("Other thing")
629
+
let asMyEnum = ("Other thing" :> myEnum)
630
+
631
+
// One
632
+
let asMyEnum = ("One" :> myEnum)
633
+
```
466
634
467
-
// TODO #731: expand this section with:
468
-
//
469
-
// Coercing between variants (and the constraints around that)
470
-
// Why you can sometimes coerce from variant to string/int/float, and how to think about that (runtime representation must match)
471
-
// The last additions of allowing coercing strings to unboxed variants with catch-all string cases
635
+
This works because the variant is unboxed **and** has a catch-all case. So, if you throw a string at this variant that's not representable by the payloadless constructors, like `"One"` or `"Two"`, it'll _always_ end up in `Other(string)`, since that case can represent any `string`.
472
636
473
637
## Tips & Tricks
474
638
@@ -620,12 +784,12 @@ switch data {
620
784
```js
621
785
console.log("Wof");
622
786
623
-
var data =/*Dog*/0;
787
+
var data ="Dog";
624
788
```
625
789
626
790
</CodeTab>
627
791
628
792
The compiler sees the variant, then
629
793
630
-
1. conceptually turns them into `type animal = 0 | 1 | 2`
794
+
1. conceptually turns them into `type animal = "Dog" | "Cat" | "Bird"`
631
795
2. compiles `switch` to a constant-time jump table (`O(1)`).
0 commit comments