How to write interop for TS Unions

Hi.
Example:

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

declare function getSmallPet(): Fish | Bird;

// second example

type Foo = { foo: string }
type Bar = { bar: string }

type GetFooOrBar = () => Foo | Bar

How can I write interop for this function?

If it works for you, simplest way to use getSmallPet from rescript would be:

type pet = {
  fly: option<() => ()>,
  swim: option<() => ()>,
  layEggs: () => ()
}

@val external getSmallPet: () => pet = "getSmallPet"

let _ = getSmallPet().layEggs()
1 Like

Then I will able to use a non-existent method. It can crash my app. Bad idea.

Hi @snatvb

In ReScript I understand that functions should return a single type, so returning Fish | Bird is a TypeScript concept but not a ReScript concept.

You might implement this using modules and Variants:

module Bird = {
  type t
}

module Fish = {
  type t
}

module Pet = {
  type t = Bird(Bird.t) | Fish(Fish.t)
  let getSmallPet = () => {
    // TODO: Logic to return type Pet.t
  }
}

The other methods would be implemented within the modules.

layEggs is not optional because in your example it always exist. If not, make it optional and check before call.

I think the answers are missing the important part of the question which is interop. I have been looking again over the past few days at interop with tagged/disjoint unions and to be honest there isn’t a good story other than writing manual encoders and decoders to convert from json (representing the TS side) into variants in rescript and vice versa. If anyone else knows any nice solutions I’m all ears, otherwise I’ll post a short example of what I’ve got so far once I’m back at a computer.

2 Likes

Often the best solution is to write a different binding for each type:

type fish
type bird
@module("foo") external getSmallPetFish: unit => fish = "getSmallPet"
@module("foo") external getSmallPetBird: unit => bird = "getSmallPet"

Most of the time, the return type can be safely determined based on the function arguments, so you would write the externals based on that. (Although not in this hypothetical example, since they just take unit, but in the real world this isn’t usually the case.)

But if it truly is impossible to statically determine what the return type is, then you can bypass the type system with raw JavaScript and do a runtime check to cast it to the appropriate type.

type bird
type fish
type t = Bird(bird) | Fish(fish)

let isBird = %raw(`function(x) { return "fly" in x }`)

@module("foo") external getSmallPetUnsafe: unit => 'a = "getSmallPet"

let getSmallPet = () => {
  let pet = getSmallPetUnsafe()
  if isBird(pet) {
    Bird(pet)
  } else {
    Fish(pet)
  }
}
4 Likes

Second way is good. Thanks.