Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Enums without properties

Enums with variants that have no properties are said to be “flat enums”.

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
enum MyAnimal {
    Cat,
    Dog,
}
}

These are represented by a similar enum in Typescript:

enum MyAnimal {
    Cat,
    Dog,
}

Constructing these in Typescript is done as usual:

const dog = MyAnimal.Dog;
const cat = MyAnimal.Cat;

Enums with properties

Rust enums variants optionally have properties. These may be name or unnamed.

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
enum MyShape {
  Point,
  Circle(f64),
  Rectangle { length: f64, width: f64, colour: String },
}
}

These may be constructed like so:

#![allow(unused)]
fn main() {
let p = MyShape::Point;
let c = MyShape::Circle(2.0);
let r = MyShape::Rectangle { length: 1.0, width: 1.0, colour: "blue".to_string(), };
}

These can be used in pattern matching, for example:

#![allow(unused)]
fn main() {
fn area(shape: MyShape) -> f64 {
  match shape {
    MyShape::Point => 0.0,
    MyShape::Circle(radius) => PI * radius * radius,
    MyShape::Rectangle { length, width, .. } => length * width,
  }
}
}

Such enums are in all sorts of places in Rust: Option, Result and Errors all use this language feature.

In Typescript, we don’t have enums with properties, but we can simulate them:

enum MyShape_Tags { Point, Circle, Rectangle };
type MyShape =
    { tag: MyShape_Tags.Point } |
    { tag: MyShape_Tags.Circle, inner: [number] } |
    { tag: MyShape_Tags.Rectangle, inner: { length: number, width: number, colour: string }};

In order to make them easier to construct, a helper object containing sealed classes implementing the tag/inner:

const point = new MyShape.Point();
const circle = new MyShape.Circle(2.0);
const rectangle = new MyShape.Circle({ length: 1.0, width: 1.0, colour: "blue" });

These are arranged so that the Typescript compiler can derive the types when you match on the tag:

function area(shape: MyShape): number {
    switch (shape.tag) {
        case MyShape_Tags.Point:
            return 0.0;
        case MyShape_Tags.Circle: {
            const [radius] = shape.inner;
            return Math.PI * radius ** 2;
        }
        case MyShape_Tags.Rectangle: {
            const [length, width] = shape.inner;
            return length * width;
        }
    }
}

instanceOf methods

Both the enum and each variant have instanceOf methods. These may be useful when you don’t need to match/switch on all variants in the Enum.

function colour(shape: MyShape): string | undefined {
    if (MyShape.Rectangle.instanceOf(shape)) {
        // We know what the type inner is.
        return shape.inner.colour;
    }
    return undefined;
}

Tip

Adding one or more properties to one or more variants moves these flat enums to being “non-flat”, as above.

To help switch between the two, the classes representing the variants have a static method new. For example, adding a property to the MyAnimal enum above:

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
enum MyAnimal {
    Cat,
    Dog(String),
}
}

This would mean changing the typescript construction to:

const dog = new MyAnimal.Dog("Fido");
const cat = new MyAnimal.Cat();

The variants each have a static new method to have a smaller diff:

const dog = MyAnimal.Dog.new("Fido");
const cat = MyAnimal.Cat.new();

Enums with explicit discriminants

Both Rust and Typescript allow you to specify discriminants to enum variants. As in other bindings for uniffi-rs, this is supported by uniffi-bindgen-react-native. For example,

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
pub enum MyEnum {
    Foo = 3,
    Bar = 4,
}
}

will cause this Typescript to be generated:

enum MyEnum {
    Foo = 3,
    Bar = 4,
}

Methods

Enums can have methods defined via #[uniffi::export] impl:

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
pub enum Direction { North, South, East, West }

#[uniffi::export]
impl Direction {
    pub fn opposite(&self) -> Direction {
        match self {
            Direction::North => Direction::South,
            Direction::South => Direction::North,
            Direction::East => Direction::West,
            Direction::West => Direction::East,
        }
    }
}
}

For flat enums, methods become static-style functions on the enum namespace. The self parameter becomes the first argument:

Direction.opposite(Direction.North);  // Direction.South
Direction.opposite(Direction.East);   // Direction.West

For tagged enums (enums with properties), methods follow the same pattern — static functions on the outer namespace, taking a variant instance as first argument:

#![allow(unused)]
fn main() {
#[derive(uniffi::Enum)]
pub enum Shape {
    Circle { radius: f64 },
    Rectangle { width: f64, height: f64 },
}

#[uniffi::export]
impl Shape {
    pub fn area(&self) -> f64 { … }
}
}
const circle = new Shape.Circle({ radius: 1.0 });
const rect = new Shape.Rectangle({ width: 3.0, height: 4.0 });

Shape.area(circle);  // Math.PI
Shape.area(rect);    // 12.0

Uniffi traits

Implementing the following traits in Rust causes the corresponding methods to be generated in Typescript:

TraitTypescript methodReturn
DisplaytoString()string
DebugtoDebugString()string
Eqequals(other)boolean
HashhashCode()bigint
OrdcompareTo(other)number (i8: −1, 0, or 1)

These are declared on the enum using the #[uniffi::export(...)] attribute:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, uniffi::Enum)]
#[uniffi::export(Debug, Display, Eq, Hash, Ord)]
pub enum TraitEnum {
    Alpha,
    Beta { val: String },
}

impl std::fmt::Display for TraitEnum {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            TraitEnum::Alpha => write!(f, "Alpha"),
            TraitEnum::Beta { val } => write!(f, "Beta({})", val),
        }
    }
}
}

For enums with properties (tagged enums), these become instance methods on each variant class:

const a = new TraitEnum.Alpha();
const b = new TraitEnum.Beta({ val: "hello" });

a.toString();         // "Alpha"
b.toString();         // "Beta(hello)"
a.toDebugString();    // "Alpha"

a.equals(new TraitEnum.Alpha());              // true
a.equals(b);                                  // false

a.compareTo(b);       // negative (Alpha sorts before Beta)
b.compareTo(a);       // positive

a.hashCode();         // bigint

For flat enums (variants with no data), the methods are generated as static functions in a namespace that merges with the enum. This keeps the enum variants as plain values:

#![allow(unused)]
fn main() {
#[derive(Debug, PartialEq, Eq, Hash, PartialOrd, Ord, uniffi::Enum)]
#[uniffi::export(Debug, Display, Eq, Hash, Ord)]
pub enum FlatTraitEnum {
    Alpha,
    Beta,
    Gamma,
}
}
// Variants are still plain enum values
const a = FlatTraitEnum.Alpha;
const b = FlatTraitEnum.Beta;

// Trait methods are static namespace functions
FlatTraitEnum.toString(a);           // "Alpha"
FlatTraitEnum.toDebugString(a);      // "Alpha"

FlatTraitEnum.equals(a, b);          // false
FlatTraitEnum.equals(a, FlatTraitEnum.Alpha);  // true

FlatTraitEnum.compareTo(a, b);       // negative (Alpha sorts before Beta)
FlatTraitEnum.compareTo(b, a);       // positive

FlatTraitEnum.hashCode(a);           // bigint