Elm-tagged and phantom types

I was excited to see the elm-tagged package because our project has a lot of replicated code to wrap string-based IDs in types and those ID’s then don’t work well with dictionaries. However, my quick experiments show that it allows forging of tagged values. Specifically, if I define something like:

type alias AccountID = Tagged.Tagged AccountID_Private String

type AccountID_Private = NotExported

Then it turns out that in another module, I can write:

forgedID : AccountID
forgedID = Tagged.tag “forged”

Is there a way to prevent this? Obviously, I could just say “don’t do that” but one could say that about all sorts of things for which type-safety instead says “you can’t do that”.


I beleive by definition there can’t be solution to this, right? By defining that it’s AccountID you basically make it one. If you need more safety you can always define type type AccountID = AccountID String. I think whole point ot Tagged is to make things like this possible.

“things like this possible” means zero overhead in construction if you define which type you want back.

I might be wrong but this is how I understand it.

My old code was exactly type AccountID = AccountID String repeated for all of the identifier types.

I was able to get a variant on the phantom types approach working just now by insisting that construction function take an instance of the phantom type. So no instances are publicly available, construction is thereby restricted.

But I suspect that this will then keep me from using something like TaggedDict since it relies on being able to reconstruct a tagged value from the underlying value.


IMHO question is if it is really less safe when you ahve to explicitelly type variable. One can say it’s as much explicit as using type constructor directly. Even box value doesn’t prevent anyone to define it like forgedID = ForgedID "forged".

From how I understand it a whole goal of phantom types is to be able to retype secretly equvalent values just by declaring which box you want with no implementation overhead of matching and constructing new types. Is there anything I’m missing?