I think we don’t need or want a general functionality for creating every kind of class in BuckleScript; we just need to interop with libraries that require their users to extend their classes. That means we just need to be able to override methods. So that is the approach I want to take.
Having said that, the question is should you write your own? Or wait for BuckleScript to provide one? Personally I feel it’s unlikely that BuckleScript will provide an extension to make JS class inheritance easier. Based on its core feature set, OOP is not something BuckleScript is focusing on. So I propose we write our own. In fact, I have written one, I’ll introduce it below.
So, here’s an example:
class Person {
toString() {
return `id: ${this.getId()}, name: ${this.getName()}`;
}
}
exports.Person = Person;
This Person
abstract class returns a nicely formatted string from a person data type, but only if you extend it and give it the getId()
and getName()
methods. I’ve written a function that does this extension using prototypal inheritance. Here’s the usage:
// Employee.re
// Bind to the superclass constructor function
[@bs.module "./person.js"] external super: _ = "Person";
let idField = "id";
let nameField = "name";
// Create our class's constructor function
let ctor(id, name) = {
open Class;
// Equivalent to `this.id = id;`
setField(this, idField, id);
setField(this, nameField, name);
};
// Does the prototypal inheritance
Class.(extend(~ctor, ~super, [
// Equivalent to `ctor.getId = function() { return this.id; };`
("getId", [@bs.this] field(_, idField)),
("getName", [@bs.this] field(_, nameField)),
]));
// We still need to bind to the inherited class from BuckleScript
type t;
[@bs.new] external make: (int, string) => t = "ctor";
[@bs.send] external toString: t => string = "";
let test() = "Bob" |> make(1) |> toString |> Js.log;
// Test in node with: `require('./src/Employee.bs.js').test()`
Here is the Class
module interface:
// Class.rei
type this;
[@bs.val] external this: this = "";
[@bs.set_index] external setField: (this, string, 'value) => unit = "";
[@bs.get_index] external field: (this, string) => 'value = "";
let extend: (~ctor: 'ctor, ~super: 'super, list((string, 'get))) => unit;
Implementation:
type this;
[@bs.val] external this: this = "";
[@bs.set_index] external setField: (this, string, 'value) => unit = "";
[@bs.get_index] external field: (this, string) => 'value = "";
type prototype;
[@bs.get] external prototype: 'a => prototype = "";
[@bs.set_index] external set: ('a, string, 'b) => unit = "";
let extend(~ctor, ~super, getters) = {
super |> prototype |> set(ctor, "prototype");
getters |> List.iter(((name, get)) =>
set(prototype(ctor), name, get));
};
Not incredibly type-safe but definitely works.