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 */