Typing Subclasses

interop

#1

How can I type subclasses in Reasonml?

I have the following in JS (expressed in flow types):

declare class Disposable {
  dispose(): void
};

declare class CompositeDisposable extends Disposable {
  constructor(...disposables: Array<Disposable>): void;
  dispose(): void;

  add(...disposables: Array<Disposable>): void;
  remove(disposable: Disposable): void;
  clear(): void;
}

and I want to create bindings for CompositeDisposable where CompositeDisposable.add can receive either CompositeDisposable or Disposable.

So far, I could get this

type disposable;
module CompositeDisposable = {
  type t;
  [@bs.new] [@bs.module "atom"] external create : unit => t = "CompositeDisposable";
  [@bs.send] external dispose : t => unit = "dispose";
  [@bs.send] external add : (t, disposable) => unit = "add";
  [@bs.send] external remove : (t, disposable) => unit = "remove";
  [@bs.send] external clear : t => unit = "clear";
};

module OtherLibrary = {
  [@bs.send] external getDisposable : unit => disposable = "getDisposable";
};

let compositeDisposable = CompositeDisposable.create();
let compositeDisposable2 = CompositeDisposable.create();
let disposable = OtherLibrary.getDisposable();

CompositeDisposable.add(compositeDisposable, disposable);
CompositeDisposable.add(compositeDisposable, compositeDisposable2);
/* ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Error:
  ------
  This has type:
    CompositeDisposable.t
  But somewhere wanted:
    disposable
*/
CompositeDisposable.dispose(compositeDisposable);

#2

Try to add function to cast CompositeDisposable.t to disposable

external asDisposable : t => disposable = "%identity";

Example here


#3

The compiler needs to be told that disposable and CompositeDisposable.t are the same type. One way to do this is by saying:

type disposable;

module CompositeDisposable = {
  type t = disposable;
  ...
};

#4

That will let you pass a disposable to functions that expect a CompositeDisposable. I’ve been reading some binding and they usually do what @maarekj suggested.


#5

FWIW I’ve been trying to find a way to do this for months and have found no other way to do it properly than to copy the entire extended class over to the new one.

You can check out this GitHub issue for more context: https://github.com/rrdelaney/ReasonablyTyped/issues/25

Let me know if you find a better way :slight_smile:


#6

Yeah, explicitly casting is probably the best way, on further thought.


#7

I think there’s a better way to do it, without using the %identity escape hatch:

type disposableClass;

class type disposable =
[@bs]
{
  pub classDisposable: disposableClass
};

type compositeDisposableClass;

class type compositeDisposable =
[@bs]
{
  inherit disposable;
  pub classCompositeDisposable = compositeDisposableClass
};

module Disposable = {
  [@bs.send] external dispose: Js.t(#disposable) => unit = "";
}

module CompositeDisposable = {
  [@bs.send] external add: (Js.t(#disposable), Js.t(#disposable)) => unit = "";
}