Search code examples
modulevisibilityrescript

How to make a type constructor private in rescript (except in current module)?


I would like to make a validation function taking a name and outputting a validName type. I don't want to be able to construct values of type ValidName outside the module without using the function validateName.

I am trying to make the ValidName type private, but it makes it impossible for me to use it in the validateName function (event if it is in the same Module).

What is the right way to do this in rescript?

Here is a playground

Here is the code:

module MyModule = {
    type notValidatedName = NotValidatedName(string)
    type validName = private ValidName(string)
    type errorMessage = string
  
    let validateName = (~name: notValidatedName): Belt.Result.t<validName, errorMessage> => {
      switch name {
      | NotValidatedName(name) when Js.String2.length(name) <= 2 => Belt.Result.Ok(ValidName(name)) // not possible because ValidName is private
      | _ => Belt.Result.Error("String is too short to be a name")
      }
    }
}

let nameToShort = MyModule.ValidName("aa")           // I don't want this to be possible
let notValidName = MyModule.NotValidatedName("aa")   // This is fine

let nameResult = MyModule.validateName(~name=notValidName)

Solution

  • Visibility is specified in module signatures (as are type annotations, typically), not on the definitions themselves. You also don't need constructors, but should instead either make the type abstract, or the type alias private.

    You can specify a module signature on a local module, as shown below, but typically you'd put it in a .resi ("interface") file. Everything you can put in a module signature you can also put in an interface file. See the docs for more.

    This is how I would do it:

    module MyModule: {
      type validName = private string
      let validateName: string => result<string, string>
    } = {
      type validName = string
    
      let validateName = name => {
        if String.length(name) <= 2 {
          Ok(name)
        } else {
          Error("String is too short to be a name")
        }
      }
    }
    
    let nameToShort: MyModule.validName = "aa" // Type error: is 'string', wanted 'MyModule.validName'
    let notValidName: string = "aa" // This is fine
    
    let nameResult = MyModule.validateName(notValidName)
    
    switch nameResult {
      | Ok("aa") => Js.log("yay!")
      | Ok(_) => Js.log("wat?")
      | Error(err) => Js.log(err)
    }
    

    A private type alias means a value of the type is treated as a string in every way, except that you cannot construct or coerce a string of that type outside the module it's defined in.

    If you make it abstract, which you do by replacing type validName = private string with just type validName, you hide the alias entirely from the outside world. The only way to interact with values of this type then is to pass it back to the module through the functions it exposes.