Help writing equivalent TypeScript code in ReScript

Hi everyone, curious JavaScript/TypeScript programmer expoloring ReScript here.

The other day I saw a talk on Domain Modeling by Scott Wlaschin. One of the problems he presented was in a nutshell: given a user type (with name, age, and so on), how do you make it so that a user has either an email, post address, or both?

I found this interesting, and I tried doing just that using TypeScript:

type Email = { email: string }
type PostAddress = { postAddress: string }
type EmailAndPostAddress = Email & PostAddress

interface User {
  name: string;
  contactInfo: Email | PostAddress | EmailAndPostAddress
}

const john: User = {
  name: "John",
  contactInfo: {
    email: "john@gmail.com"
  }
}

const peter: User = {
  name: "Peter",
  contactInfo: {
    postAddress: "334 New York"
  }
}

const mary: User = {
  name: "Mary",
  contactInfo: {
    postAddress: "334 New York",
    email: "mary@gmail.com"
  }
}

I then tried writing the same in ReScript, but got stuck with something I expected to work:

type email = Email(string)
type postAddress = PostAddress(string)
type emailAndPostAddress = EmailAndPostAddress(email, postAddress)

type user = {
  name: string, 
  contactInfo: email | postAddress | emailAndPostAddress
}

The compiler was not happy with me. I’m probably missing something obvious here, but I’m hoping someone is able to explain. I don’t have much experience with algebraic types as you might have guessed. Thanks.

You would probably want to do something like this in ReScript:

type email = string
type postAddress = string
type contactInfo =
  | Email(email)
  | PostAddress(postAddress)
  | EmailAndPostAddress(email, postAddress)

type user = {
  name: string,
  contactInfo: contactInfo,
}

The difference is contactInfo is one type. You can’t have a value in a record which could be one of several types. You need to have one variant type with different payloads in it’s constructors.

If you’d want to follow along with Wlaschin’s ideas you’d have an interface file making the types postAddress and email opaque. This way you could have a function to create values of such type, which would validate the input and return a result.

Thanks! I’m having some trouble getting my head around the type system, but I must say it is quite interesting coming from a background consisting mostly of JavaScript, Python and Java :smiley:

2 Likes

I tried to expand on this for the sake of practice/completeness - we’d end up with something like this I would assume?:

type email = string
type postAddress = string
type contactInfo =
  | Email(email)
  | PostAddress(postAddress)
  | EmailAndPostAddress(email, postAddress)

type user = {
  name: string,
  contactInfo: contactInfo,
}

let mary1: user = {
  name: "Mary",
  contactInfo: PostAddress("334 New York")
}

let mary2: user = {
  name: "Mary",
  contactInfo: Email("mary@gmail.com")
}

let mary3: user = {
  name: "Mary",
  contactInfo: EmailAndPostAddress("334 New York", "mary@gmail.com")
}
1 Like

You’re correct. You could potentially skip the type annotations of maryX.
When you use a record, the type definition has to be in scope to be usable. (In this case type user is in the same module.) If the type definition wouldn’t be in scope, the compiler would error out, saying the The record field name can't be found.

Takling about inference of record types… simply said: (without type annotations) The compiler just walks the module upwards and looks for the first “matching” type definition. - “matching” meaning all fieldNames match the used one

1 Like

Cool, thank you. I didn’t know that tidbit of the first matching type thanks for sharing!