Noob question about type guards

Hello,

I have been following the progress for more than a year, and last weekend I finally went ahead and tried to convert some functions in a Word file parser that is currently written in Flow.

I have a broad experience with JavaScript and TypeScript, but I’m still quite the noob when it comes to ReScript. I haven’t found an immediate answer to my question from reading the docs or searching the forum, but please tell me if I might have missed something.

I’m wondering how to idiomatically go from an array where items are typed as a variant (or union in TypeScript) to an array that has been filtered on one case of the variant, i.e. has its type narrowed.

To give a high-level description: a common pattern in my codebase is to start with an array where each item can have one of several types (typed in TypeScript as A | B | C). I have understood that in ReScript, the correct type to use would be a variant.

In TypeScript, it is possible to filter the array with a type guard, such that the compiler can infer that the elements of the resulting array have had their types narrowed to one of A, B or C.

In ReScript, however, I have tried modelling this with a variant type, switch and Js.Array2.filter — but I haven’t figured out how to let the compiler infer that the types have been narrowed.

type rec element = {
  elements: array<element>,
  name: string,
  \"type": string,
}

type textElement = {text: string, \"type": [#text]}

type e = Element(element) | TextElement(textElement)

type argument = {
  elements: option<array<e>>
}

/**
 * Extracts text from a OOXML leaf element.
 *
 * @returns extracted text
 */
let default = ({ elements }: argument) => {
  switch elements {
  | Some(elements) =>
    elements
    ->Js.Array2.filter(item =>
      switch item {
      | TextElement(item) => true
      | _ => false
      }
    )
    ->Js.Array2.map(item => item.text) // `.text` is not allowed here
  | None => undefined
  }
}

Could someone please enlighten me?

Thank you! :pray:

I believe Belt.Array | ReScript API will do the job.

3 Likes

The issue is that Js.Array2.filter returns an array of the same type. It’s removing any items that use the TextElement constructor, but the resulting array still is of type e, which could still theoretically be Element or TextElement. Type “narrowing” like that doesn’t exist in nominal type systems like what ReScript uses.

What you need to do is return an array which is a completely different type. One way to reason about problems like this in ReScript is ask yourself “what type signature do I need to implement?” In this case, we want a function that takes an array of type e and return an array of type textElement, e.g: array<e> => array<textElement>

We can use Belt.Array.keepMap (what @yawaramin linked) to achieve this.

let default = ({ elements }: argument) => {
  switch elements {
  | Some(elements) =>
    elements
--    ->Js.Array2.filter(item =>
++    ->Belt.Array.keepMap(item =>
      switch item {
--      | TextElement(item) => true
--      | _ => false
++      | TextElement(item) => Some(item)
++      | _ => None
      }
    )
    ->Js.Array2.map(item => item.text) // `.text` is not allowed here
  | None => undefined
  }
}

playground link.

You can also remove the second Js.Array2.map step by just getting the text in keepMap:

Belt.Array.keepMap(elements, item =>
  switch item {
  | TextElement(item) => Some(item.text)
  | _ => None
  }
)
2 Likes

Thanks @yawaramin and @johnj for your quick answers. The provided suggestion was very helpful, the .res file compiles now.

I simplified the question above: actually, the function needs to return a string, which created by joining the array elements. For this, I could use

  • Js.Array.join("") → deprecated, but this would be my immediate guess based on my JavaScript knowledge
  • Js.Array.joinWith(""), but this one leads the compiler to underline the Belt.Array.keepMap above with the error “This has type: array. Somewhere wanted: string”
  • Belt.Array.joinWith("", a => a) which compiles

Both my confusion with filter above and join here leads me to think that it would be very helpful if the documentation contained recipes for

  1. commonly occurring patterns in JavaScript/TypeScript, (such as how to model filtering an array with type guards)
  2. counterparts to JavaScript methods such as all object and array methods.

I would very much like to help improve the docs; but for now, my ReScript knowledge is insufficient. I think it would be very helpful to highlight a single “recommended” way to of doing things (e.g. which alternative to choose between Belt.Array, Js.Array and Js.Array2).

I could offer further suggestions such as to rename utility functions to exactly match their JS counterparts, and to eliminate duplicate functionality. But I realise that my understanding of ReScript is incomplete, and that there’s probably a reason why the current API is designed as it is.

1 Like

A side comment, \"" is strongly discouraged, it breaks the compiler invariants silently.

You can do it more elegantly:

type rec element = {
  elements: array<element>,
  name: string,
  @as("type")
  typ: string,
}

see the output here: https://rescript-lang.org/try?code=C4TwDgpgBAThDGUIBsIFsIDthQLxQG8AoKJVDbAZwC4oBDGGOkAHhXS2AD4AaEqTHQy1KwGAEtMAcz6kAAnUoAKAEShIKgJT91IsZJlEAvkSKocAV1L4C7CsEpRaAbQC6PAUIjUVADxUeulB+KkZAA

1 Like

Javascript (and by extension typescript) are languages in which you can write your program and then try to fit types in at a later stage. Rescript requires you to adapt a duality: You are building up a program and a type at the same time. One side, program or type, will induce what can be done on the other side.

Usually, if you have a type such as array<e>, you are looking for functions which transforms it, maybe to array<string>, and then you are looking at a function which transforms that further: array<string> => string, or the more general variant in Belt.Array.joinWith : (array<string>, string, string => string) => string. You can also be lucky that a library provides a way to consume an array of strings, i.e., arr->Belt.Array.keepMap(f)->React.array. Finally, in the concrete case, there’s also ""->Js.String2.concatMany(arr) because we aren’t interspersing the array with a separator.

In the case of e.g., filter, the type tells a story: filter: (array<'a>, 'a => bool) => array<'a>. Note that in the input to the function we have a type variable 'a and that it is also present in the output type on the RHS of the final =>. This means “filter cannot change the type of what it processes. What is going into the filter is what comes out of the filter”. So we either have to compose filter with another function, or use keepMap from Belt which allows for manipulating the array as it is being processed. I think I prefer keepMap here, for reasons which digresses into discussions of total/partial functions.

At a certain point, you start looking not for function names, but for types which match your desired transformation. I wanna go from a type 'a to a type 'b and how can I do that? This is a fine art, heavily guided by a few things:

  • Intuition: Once you know a library, you know enough LEGO™ building blocks so you can start solving the puzzle without having to go look up things all the time. You also develop a strong hunch as to which direction you should look for a solution.
  • Rules and laws: While we can jot down any function we want, it turns out that some functions tend to be stronger. They simply allow for better composition. One particular strategy which has been used a lot in the functional programming community is to look at math. A function which follows some abstract principle has the advantage that the abstraction foster composition. As a programmer, you start thinking a bit more abstractly, which also means you can use this as a guide for the functions that has to be in a library.

One tool which comes to mind is Haskell’s Hoogle. You input either a function, or an approximate type. Hoogle then searches for functions which match the criteria. Hoogle understands type variables such as 'a and is able to plug in a concrete type, say a string. It feels much like using a normal search engine and you have to make sure the function does what you want. But it’s an invaluable resource if you are trying to establish familiarity in a new library space.

The crux is this: programming with types is different from programming and then making the type system happy. Typescript is nice because it gives Javascript programmers a foot in the door so to speak. But it also means there is going to be some impedance between the paradigms we can’t entirely get rid of, ever.

A cookbook of sorts would be nice to have. Take some common construct which is idiomatic in Javascript. Describe what makes it hard to type properly, because this is often the key problem. Then suggest solutions. If it’s really common, provide a library targeting the transformation (OCamls ctypes library comes to mind). If it’s hard to provide in a library, improve the Rescript FFI to target the transformation specifically.

5 Likes