Typescript Cats Example in Rescript

I was looking up the Record type on typescriptlang.org. What would be the best way to convert this type to rescript keeping in mind interop, for example a binding.

interface CatInfo {
  age: number;
  breed: string;
}

type CatName = "miffy" | "boris" | "mordred";

const cats: Record<CatName, CatInfo> = {
  miffy: { age: 10, breed: "Persian" },
  boris: { age: 5, breed: "Maine Coon" },
  mordred: { age: 16, breed: "British Shorthair" },
};

cats.boris;

If I am reading this right, the record type has to have one of the names from type CatName and has to have the properties of type Catinfo.

Can we do this type of construction?

I came up with this but doesnt seem quite the same thing, playground:

type catInfo = {
  age: int,
  breed: string,
}

type record =
  | Miffy(catInfo)
  | Boris(catInfo)
  | Mordred(catInfo)

let miffy = Miffy({age: 10, breed: "Persian"})

let boris = Boris({age: 5, breed: "Maine Coon"})

let mordred = Mordred({age: 16, breed: "British Shorthair"})

let cats: array<record> = [miffy, boris, mordred]
Js.log(cats)

and logs this:

[
  { TAG: 0, _0: { age: 10, breed: 'Persian' } },
  { TAG: 1, _0: { age: 5, breed: 'Maine Coon' } },
  { TAG: 2, _0: { age: 16, breed: 'British Shorthair' } }
]

ts version logs, playground:

{
  "miffy": {
    "age": 10,
    "breed": "Persian"
  },
  "boris": {
    "age": 5,
    "breed": "Maine Coon"
  },
  "mordred": {
    "age": 16,
    "breed": "British Shorthair"
  }
} 

Or logging console.log(cats.boris) in ts:

{
  "age": 5,
  "breed": "Maine Coon"
} 

Which my structure doesnt let us do.

Any musings on this type of thing would be appreciated.

Thank you.

What you’re trying to do looks similar to Js.Dict but with a more constrained key type.

I’d probably build my own module by copying most of the code from Js.Dict. Here is a start:

module Dict = {
  type t<'k, 'v>

  @obj external empty: unit => t<'k, 'v> = ""

  @get_index
  external unsafeGet: (t<'k, 'v>, 'k) => 'v = ""

  let get = (type v, dict: t<'k, v>, k: 'k): option<v> =>
    if %raw(`k in dict`) {
      Some(unsafeGet(dict, k))
    } else {
      None
    }

  @set_index
  external set: (t<'k, 'v>, 'k, 'v) => unit = ""
}

type catInfo = {
  age: int,
  breed: string,
}

let cats: Dict.t<[#miffy | #boris | #mordred], catInfo> = Dict.empty()

cats->Dict.set(#miffy, {age: 10, breed: "Persian"})

let miffy = cats->Dict.get(#miffy)

To get Js.Dict source in ReScript for reference I did this:

cat node_modules/rescript/lib/ocaml/js_dict.ml | npx rescript format -stdin .ml > Dict.res
5 Likes

Great answer! Thanks for taking the time @rpominov.

Bonus love for the dev workflow tip at the bottom.

What does the type v do in let get =.... I have seen this every now and then but never understood it? It not defined previously so are we declaring right there?

1 Like

I don’t fully understand this myself to be honest. It means “for any type v …”, but I don’t know what is the difference between (type a, x: a): a => x and (x: 'a): 'a => x. Seems like sometimes you have to define it explicitly.

I hope someone else can give a better answer :slight_smile:

1 Like

I really don’t understand what you’re supposed to do with such a data structure, but given your fields are not dynamic, I’d just use a record for this:

type catInfo = {
  age: number,
  breed: string,
}

type cats = { 
  miffy: catInfo,
  boris: catInfo,
  mordred: catInfo,
}

But still, I’m not sure what’s the point.

1 Like

Was trying to understand the equivelant to the linked ts code above. Your answer is a nice alternative if you know what the keys are. playgroud

Lets you do the cats.miffy to get the value.

type catInfo = {
  age: int,
  breed: string,
}

type cats = { 
  miffy: catInfo,
  boris: catInfo,
  mordred: catInfo,
}

let cats = {
  miffy: {
    age: 2,
    breed: "tabby",
  },
  boris: {
    age: 1,
    breed: "persian",
  },
  mordred: {
    age: 3,
    breed: "siamese",
  },
}
Js.log2("cats", cats)
Js.log2("cats.miffy", cats.miffy)

output

cats {
  miffy: { age: 2, breed: 'tabby' },
  boris: { age: 1, breed: 'persian' },
  mordred: { age: 3, breed: 'siamese' }
}
cats.miffy { age: 2, breed: 'tabby' }

Thanks for participating! Would be nice to be able to tick Solution twice for multiple good answers.

@tsnobip do you know anything about :point_up:

I think this can be useful if you want to write code that operates on records with arbitrary but still restricted keys. But yeah, it’s hard to come up with a real world example, and if a regular record does the job, I’d use it instead.

I looked more into type v. If I understand correctly, it’s called “locally abstract type” in OCaml, and it can be useful when you deal with GADTs or recursion. Not sure why it was used in Js.Dict, where I copied it from. let get = (dict: t<'k, 'v>, k: 'k): option<'v> seem to work fine too.

@rpominov Agree with you. Your solution more directly answers the question but the other seems worth trying to make work. The @tsnobip solution would work if we changed cats to a variant which could be one of miffy, boris or mordred.

Will keep an eye out for type v.

Thank you, sir.

I think you can get really close to the “spirit” of the TS demo -

type catinfo = {
  age: int,
  breed: string
}

type catname = [#miffy | #boris | #mordred]

Already you can see ReScript has a better option for handling simple variants. Instead of forcing everything through strings like TS, ReScript supports polymorphic variants

To create cats we reach for Belt.Map. We will construct our map using Map.fromArray which takes an array of ('k, 'v) tuples where 'k can be any type. This differs from TS again where you are forced to use a string -

open Belt

let cats = Map.fromArray([
  (#miffy, { age: 10, breed: "Persian" }),
  (#boris, { age: 5, breed: "Maine Coon" }),
  (#mordred, { age: 16, breed: "British Shorthair" }),
], ~id=module(CatCompare))

This freedom is possible because we tell Belt.Map how to “key” our cats using a CatCompare module that we create using Belt.Id.MakeComparable. We want to key the cats by catname, so we specify that for our module’s t -

module CatCompare = Id.MakeComparable({
  type t = catname // ✅
  let cmp = Pervasives.compare
})

Now you can perform lookup in cats using Map.get any catname

exception NotFound

switch cats->Map.get(#boris) {
  | Some(cat) => Js.log(cat)
  | None => raise(NotFound)
}

We can see cats compiles cleanly as -

var cats = Belt_Map.fromArray([
      [
        "miffy",
        {
          age: 10,
          breed: "Persian"
        }
      ],
      [
        "boris",
        {
          age: 5,
          breed: "Maine Coon"
        }
      ],
      [
        "mordred",
        {
          age: 16,
          breed: "British Shorthair"
        }
      ]
    ], CatCompare);

I think this approach is close to how Rescript is intended to be used. It has great ergonomics and afford you more flexibility. @rpominov’s answer is also good but the use of %raw is :frowning_face: and Js.Nullable should be used instead. Being asked to define a complex module every time you want a compound data type is not scalable. This is also an area where Elm suffers and OCaml’s functors shine brightly.

4 Likes

This is a grand first time post. Thanks for taking the time to teach, boss.

This data model is all kinds of weird. Theres an open collection of breeds and only three names?
Surely we’re considering more individual cats than just these three?

type breed = [ #Persian | #MaineCoon | #BritishShorthair ]
type cat = {
  breed: breed,
  name: string,
  age: int,
}

let cats = [
  { breed: #Persian,
    name: "miffy",
    age: 10,
}]

let byName = Js.Dict.fromArray( cats->Js.Array2.map(c => (c.name, c)))

Creating a separate dict implementation for this seems like we’re wagging a dog

If we have a closed set of names in a variant, a function with a switch will also do nicely?

1 Like

instead of stealing the implementation for Js.Dict what about a facade?

module type IsString = {
  type t 
  let toString: t => string  // often %identity
  let unsafeFromString: string => t // often %identity
  let unsafeFromStringArray: array<string> => array<t> // often %identity
}

module D = (St: IsString) => {
  type t<'a> = Js.Dict.t<'a>
  type key = St.t  

  let empty = Js.Dict.empty

  let get = (t, key) => Js.Dict.get(t, St.toString(key))
  let unsafeGet = (t, key) => Js.Dict.unsafeGet(t, St.toString(key))
  let set = (t, key, a) => Js.Dict.set(t, St.toString(key), a)
  let keys = (t) => Js.Dict.keys(t)->St.unsafeFromStringArray
  let entries = (t) => Js.Dict.entries(t)->Js.Array2.map(((key,value)) => (St.unsafeFromString(key), value))
  let values = Js.Dict.values

  // let fromList: list<(key, 'a)> => t<'a>
  // let fromArray: array<(key, 'a)> => t<'a>
  // let map: ((. 'a) => 'b, t<'a>) => t<'b>
}
3 Likes