From 8bf3778ec52b7293c7b501d18cdb20d5321275b2 Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Wed, 21 Oct 2026 20:47:03 +0200 Subject: [PATCH 1/2] docs: model class inheritance with phantom types Replace private aliases and explicit coercions with phantom type parameters constrained by polymorphic variants. Keep the Animal, Mammal, and Dog JavaScript hierarchy and method bindings, allowing Dog values to call parent methods directly. Update the interface example and move the cross-link to the type parameter section. --- .../docs/manual/bind-to-js-object.mdx | 36 +++++++++---------- apps/docs/markdown-pages/docs/manual/type.mdx | 4 +-- 2 files changed, 19 insertions(+), 21 deletions(-) diff --git a/apps/docs/markdown-pages/docs/manual/bind-to-js-object.mdx b/apps/docs/markdown-pages/docs/manual/bind-to-js-object.mdx index ee1255483..aefe3f84f 100644 --- a/apps/docs/markdown-pages/docs/manual/bind-to-js-object.mdx +++ b/apps/docs/markdown-pages/docs/manual/bind-to-js-object.mdx @@ -219,7 +219,7 @@ export { myBook }; ### Represent Class Inheritance -ReScript has no class declarations, but bindings can model existing JavaScript inheritance using **private type abbreviations**. Suppose `animals.js` exports the hierarchy `Animal → Mammal → Dog`: +ReScript has no class declarations, but bindings can model existing JavaScript inheritance using **phantom type parameters** and [polymorphic variants](./polymorphic-variant.mdx#lower-bound-). Suppose `animals.js` exports the hierarchy `Animal → Mammal → Dog`: ```js // animals.js @@ -245,49 +245,47 @@ export class Dog extends Mammal { Declare the base type and its methods in `Animal.res`: ```res file=Animal.res -type t +type t<'a> -@send external getName: t => string = "getName" +@send external getName: t<'a> => string = "getName" ``` -In `Mammal.res`, relate the subclass to its parent: +The **phantom type parameter** `'a` tracks the class in the type system without adding data to the JavaScript object. + +In `Mammal.res`, require the `#Mammal` tag while allowing subclasses to add their own tags: ```res file=Mammal.res -type t = private Animal.t +type t<'a> = Animal.t<[> #Mammal] as 'a> ``` -Repeat for `Dog.res`, adding its constructor and methods: +The constraint `[> #Mammal] as 'a` means `'a` must include at least `#Mammal`. + +In `Dog.res`, close the set of tags and add its constructor and methods: ```res file=Dog.res -type t = private Mammal.t +type t = Mammal.t<[#Dog | #Mammal]> @new @module("./animals.js") external make: string => t = "Dog" @send external bark: t => string = "bark" ``` -Use `:>` to **coerce** a subclass instance to a parent type: +Parent methods accept subclass instances directly: ```res let dog = Dog.make("Rex") -let mammal = (dog :> Mammal.t) -let animal = (dog :> Animal.t) -let name = animal->Animal.getName +let name = dog->Animal.getName let sound = dog->Dog.bark ``` -Coercion works through the hierarchy: `Dog.t` can be coerced directly to `Animal.t`. It changes only the type; all three variables refer to the same JavaScript object. +`Animal.getName` accepts any `Animal.t<'a>`, including `Dog.t`. `Dog.bark` accepts only `Dog.t`, so it cannot be called on other animals. -Unlike a regular [type alias](./type.mdx#type-alias), `private` keeps the types distinct. Coercion requires `:>`; a `:` annotation alone is not enough. The reverse conversion from `Animal.t` to `Dog.t` is rejected, so keep the original `Dog.t` value if you need dog-specific methods. - -Private abbreviations describe an existing inheritance relationship; they do not create inheritance or check instances at runtime. +The tags are used only during type checking. The generated JavaScript uses the original class instances. #### Interfaces -If you add a [`.resi` interface](./module.mdx#signatures), expose the same relationship. For example, `Mammal.resi` should contain: +If you add a [`.resi` interface](./module.mdx#signatures), expose the same type parameter and alias so callers can use parent methods. For example, `Mammal.resi` should contain: ```res file=Mammal.resi -type t = private Animal.t +type t<'a> = Animal.t<[> #Mammal] as 'a> ``` - -Exposing only `type t` would hide the relationship and prevent callers from coercing `Mammal.t` to `Animal.t`. diff --git a/apps/docs/markdown-pages/docs/manual/type.mdx b/apps/docs/markdown-pages/docs/manual/type.mdx index 205500306..f5f43f342 100644 --- a/apps/docs/markdown-pages/docs/manual/type.mdx +++ b/apps/docs/markdown-pages/docs/manual/type.mdx @@ -113,8 +113,6 @@ export { x }; -For JavaScript bindings, [private type abbreviations](./bind-to-js-object.mdx#represent-class-inheritance) can preserve distinct types while allowing explicit coercion to a parent type, as needed for class inheritance. - ## Type Parameter (Aka Generic) Types can accept parameters, akin to generics in other languages. The parameters' names **need** to start with `'`. @@ -234,6 +232,8 @@ export { payloadResults }; +Type parameters can also track information used only during type checking. These **phantom types** can model [JavaScript class inheritance](./bind-to-js-object.mdx#represent-class-inheritance) in bindings. + ## Recursive Types Just like a function, a type can reference itself within itself using `rec`: From 2290e75c5e97276f95d4fce5d99098dfd507324e Mon Sep 17 00:00:00 2001 From: Florian Hammerschmidt Date: Wed, 21 Oct 2026 21:02:23 +0200 Subject: [PATCH 2/2] docs: link lower bounds to class inheritance example Point readers of polymorphic variant lower bounds to the practical inheritance binding example that combines them with phantom type parameters. --- apps/docs/markdown-pages/docs/manual/polymorphic-variant.mdx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/docs/markdown-pages/docs/manual/polymorphic-variant.mdx b/apps/docs/markdown-pages/docs/manual/polymorphic-variant.mdx index f19c537e0..cb37f302a 100644 --- a/apps/docs/markdown-pages/docs/manual/polymorphic-variant.mdx +++ b/apps/docs/markdown-pages/docs/manual/polymorphic-variant.mdx @@ -378,6 +378,8 @@ Here, the compiler will enforce the user to define `#Blue | #DeepBlue | #LightBl **Note:** Since we want to define an extensible poly variant, we need to provide a type placeholder `<'a>`, and also add `as 'a` after the poly variant declaration, which essentially means: "Given type `'a` is constraint to the minimum set of constructors (`#Blue | #DeepBlue | #LightBlue`) defined in `basicBlueTone`". +For a practical use of lower bounds with phantom type parameters, see [representing JavaScript class inheritance](./bind-to-js-object.mdx#represent-class-inheritance). + ### Upper Bound `[<` The upper bound works in the opposite way than a lower bound: the extending type may only use constructors that are stated in the upper bound constraint.