How can i achieve Smart Constructor pattern from F# in ReScript?

Hello there :smiley:

I wonder if it is possible to implement smart constructor like in F#
I read some thread on this forum about private constructor, and this compile:

type someType = private SomeType(string)

At first, i thought that i could do the same thing i do in F#, but i saw that when you declare type this way, you cannot create them after, unlike F# (when you declare a module with the same name that the variant type).
I would like to restrict the use of constructor variant in order to prevent invalid state in my domain.
Like in F#, i would like to only be able to use a factory method to create such types.
Do you have advice about a way to do this in ReScript ??

Best regards :wink:

sure, this can be easily done and is a great way to have both safety and ease of use.
The trick is to declare the type private in the interface but not in the implementation:

module Foo: {
  type t = private Foo(string)
  let fromString: string => option<t>
} = {
  type t = Foo(string)
  let fromString = s =>
    /* some logic here */ switch s {
    | "Bar" => None
    | other => Some(Foo(other))
    }
}
10 Likes

You made my entire week :grinning_face_with_smiling_eyes:

@tsnobip Thx a lot !!!

1 Like

With ReScript you don’t even need the Foo wrapper variant, because the compiler enforces type safety. The wrapper variant approach became popular in F# because it doesn’t have the level of type safety ReScript does.

5 Likes

@carere, @yawaramin is right, you don’t even need the wrapper in Rescript, you can just do:

module Foo: {
  type t = private string
  let fromString: string => option<t>
} = {
  type t = string
  let fromString = s =>
    /* some logic here */ switch s {
    | "Bar" => None
    | other => Some(other)
    }
}

You can then coerce it back to string when you want like that:

let fooStr: string = (foo :> string)

The only benefit of the wrapper is that you can use pattern matching or destructuring to get the inner type back, which requires one less piece of semantic to remember.

Note you can also unbox a wrapped type so it’s represented as its inner type in JS:

module Foo: {
  @unboxed type t = private Foo(string)
  let fromString: string => option<t>
} = {
  @unboxed type t = Foo(string)
  let fromString = s =>
    /* some logic here */ switch s {
    | "Bar" => None
    | other => Some(Foo(other))
    }
}

Private and unboxed wrapped types being represented as plain types in JS, this allows to write bindings using them that give you both enhanced type-safety and ease-of-use when you want to convert back to the inner type.

It’s a great way to represent types that are strings under the hood but should not be mixed, like UUID for example.

1 Like