Using phantom data types in ReScript

Here’s a quick example of how to use phantom data types in ReScript.

I’ll show an ID module whose ID.t<'a> type (where 'a is a phantom type parameter) is used by other modules to define their own id types.

These id types will be treated as distinct by ReScript’s type system even though they will be stored as unboxed strings.

ID module

// ID.res

type t<'a> = string

@val @scope("crypto")
external randomUUID: unit => string = "randomUUID"

let generate = () => randomUUID()

let toString = id => id
// ID.resi

/**
 The type parameter is used as a phantom type to differentiate between
 different kinds of IDs
*/
type t<'a>

let generate: unit => t<'a>
let toString: t<'a> => string

Using ID.t

// User.res

// Phantom marker
type userID
type id = ID.t<userID>

external idFromString: string => id = "%identity"
// Product.res

// Phantom marker
type productID
type id = ID.t<productID>

external idFromString: string => id = "%identity"

Although User.id and Product.id are unboxed (i.e. stored as strings), trying to use one when the other is expected will not work as the type system sees them as distinct types:

let userID = User.idFromString("user-1")
let productID = Product.idFromString("product-1")


let areSameID = userID == productID
/*
 Error:
 This has type: Product.id
  But it's being compared to something of type: User.id

  You can only compare things of the same type. */
  

let getUser = (userID: User.id) => {"id": userID, "name": "Some User"}
getUser(productID)
/*
 Error:
 This has type: Product.id
  But this function argument is expecting: User.id */

(ReScript playground link)