Docs / Language Manual / Variant

You are currently looking at the v8.0.0 docs (Reason v3.6 syntax edition). You can find the latest manual page here.

Variant

So far, most of ReScript's data structures might look familiar to you. This section introduces an extremely important, and perhaps unfamiliar, data structure: variant.

Most data structures in most languages are about "this and that". A variant allows us to express "this or that".

Reason (Old Syntax)ML (Older Syntax)JS Output
 
type myResponse =
  | Yes
  | No
  | PrettyMuch;

let areYouCrushingIt = Yes;

myResponse is a variant type with the cases Yes, No and PrettyMuch, which are called "variant constructors" (or "variant tag"). The | bar separates each constructor.

Note: a variant's constructors need to be capitalized.

Variant Needs an Explicit Definition

If the variant you're using is in a different file, bring it into scope like you'd do for a record:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
// Zoo.re
type animal = Dog | Cat | Bird;
Reason (Old Syntax)ML (Older Syntax)JS Output
 
// Example.re
let pet: Zoo.animal = Dog; // preferred
// or
let pet2 = Zoo.Dog;

Constructor Arguments

A variant's constructors can hold extra data separated by comma.

Reason (Old Syntax)ML (Older Syntax)JS Output
 
type account =
  | None
  | Instagram(string)
  | Facebook(string, int);

Here, Instagram holds a string, and Facebook holds a string and an int. Usage:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
let myAccount = Facebook("Josh", 26);
let friendAccount = Instagram("Jenny");

Records in Variants

You can use a record type in a variant:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
type idType = {name: string, password: string};

type user =
  | Number(int)
  | Id(idType);

If the record type is used only in the variant definition, you may put it in line:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
type user =
  | Number(int)
  | Id({name: string, password: string});

Pattern Matching On Variant

See the Pattern Matching/Destructuring section later.

Tips & Tricks

Be careful not to confuse a constructor carrying 2 arguments with a constructor carrying a single tuple argument:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
type account =
  | Facebook(string, int); // 2 arguments
type account2 =
  | Instagram((string, int)); // 1 argument - happens to be a 2-tuple

If you come from an untyped language, you might be tempted to try type myType = int | string. This isn't possible in ReScript; you'd have to give each branch a constructor: type myType = Int(int) | String(string). The former looks nice, but causes lots of trouble down the line.

Interop with JavaScript

This section assumes knowledge about our JavaScript interop. Skip this if you haven't felt the itch to use variants for wrapping JS functions yet.

Quite a few JS libraries use functions that can accept many types of arguments. In these cases, it's very tempting to model them as variants. For example, suppose there's a myLibrary.draw JS function that takes in either a number or a string. You might be tempted to bind it like so:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
// reserved for internal usage
[@bs.module "myLibrary"] external draw: 'a => unit = "draw";

type animal =
  | MyFloat(float)
  | MyString(string);

let betterDraw = (animal) =>
  switch (animal) {
  | MyFloat(f) => draw(f)
  | MyString(s) => draw(s)
  };

betterDraw(MyFloat(1.5));

You could definitely do that, but there are better ways! For example, define two externals that both compile to the same JS call:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
[@bs.module "myLibrary"] external drawFloat: float => unit = "draw";
[@bs.module "myLibrary"] external drawString: string => unit = "draw";

ReScript also provides a few other ways to do this.

Variant Types Are Found By Field Name

Please refer to this record section. Variants are the same: a function can't accept an arbitrary constructor shared by two different variants. Again, such feature exists; it's called a polymorphic variant. We'll talk about this in the future =).

Design Decisions

Variants, in their many forms (polymorphic variant, open variant, GADT, etc.), are likely the feature of a type system such as ReScript's. The aforementioned option variant, for example, obliterates the need for nullable types, a major source of bugs in other languages. Philosophically speaking, a problem is composed of many possible branches/conditions. Mishandling these conditions is the majority of what we call bugs. A type system doesn't magically eliminate bugs; it points out the unhandled conditions and asks you to cover them*. The ability to model "this or that" correctly is crucial.

For example, some folks wonder how the type system can safely eliminate badly formatted JSON data from propagating into their program. They don't, not by themselves! But if the parser returns the option type None | Some(actualData), then you'd have to handle the None case explicitly in later call sites. That's all there is.

Performance-wise, a variant can potentially tremendously speed up your program's logic. Here's a piece of JavaScript:

JS
let data = 'dog' if (data === 'dog') { ... } else if (data === 'cat') { ... } else if (data === 'bird') { ... }

There's a linear amount of branch checking here (O(n)). Compare this to using a ReScript variant:

Reason (Old Syntax)ML (Older Syntax)JS Output
 
type animal = Dog | Cat | Bird;
let data = Dog
switch (data) {
| Dog => Js.log("Wof")
| Cat => Js.log("Meow")
| Bird => Js.log("Kashiiin")
}

The compiler sees the variant, then

  1. conceptually turns them into type animal = 0 | 1 | 2

  2. compiles switch to a constant-time jump table (O(1)).