'Int' is fine. We don't need ActualInt and IPromiseItsReallyAnInt.
'Int' is fine. We don't need ActualInt and IPromiseItsReallyAnInt.
I assumed there is a typical registration process. Future users of your application input a text in a field that is meant to be a reachable email address. So the first step would be to filter out garbage or typoed text that your mail-sending function would not be able to handle. And because you don't know how long people need to click on a link in their mail you need to store and work with that validated-but-unverified email address till they do. And for this having your own type that cannot be mixed up with the verified email addresses can make sense. Depending on how complex your application is.
These so called aggregates can only be created from a factory to avoid bad initialization. This way you never need to make ad hoc validations to be “ really really reallysure”.
Somewhat less bad would be IEEE 754 floating point numbers - where your floating-point "numbers" can include both +0 and -0, sub-normal numbers, infinities, NaNs, and other miseries.
Then program using very poor analogies, like Int.
String->(valid)Email is 'Parse', (invalid)Email->ValidEmail is 'Validate'.
> That way you never have to guess whether the email adress is unchecked
Now you just get to guess whether, for every class, there is actually a ValidClass that you were supposed to be using, instead of Class.
authenticate(User user);
^ This is a bug. We just let a malicious User object in because we invested extra time, effort and sloc into making both a User and a ValidUser class, disregarding the first "simple idea" from the article:>> 1. Use a data structure that makes illegal states unrepresentable
Or as I wrote:
> only make classes for valid objects
The type system stops this. You can't pass, e.g. a User to authenticate because it only accepts a ValidUser.
> only make classes for valid objects
Using primitives (String, Int, etc.) for unvalidated input, and custom classes for validated input is fine in many cases. However, sometimes you need to represent data that is in the process of being validated (e.g. when validation takes time, like waiting for a user to validate their email address) and then these intermediate classes arrive.
Be careful about how the email is implicitly verified though, and how different login methods and assumptions interact when for example migrating users from one system or platform to another.
https://krebsonsecurity.com/2024/07/researchers-weak-securit...
> analysis released by security experts at Metamask and Paradigm finds the most likely explanation for what happened is that Squarespace assumed all users migrating from Google Domains would select the social login options — such “Continue with Google” or “Continue with Apple” — as opposed to the “Continue with email” choice
> Squarespace never accounted for the possibility that a threat actor might sign up for an account using an email associated with a recently-migrated domain before the legitimate email holder created the account themselves
> since there’s no password on the account, it just shoots them to the ‘create password for your new account’ flow. And since the account is half-initialized on the backend, they now have access to the domain in question
This kind of mistake makes me think that it’s better to be super explicit about the state of everything when it comes to accounts.
So even if it may seem excessive to have VerifiedEmail as a type instead of marking the account as verified or not. I will prefer being explicit.
And in medium to big size systems with multiple pieces of user account data that require separately keeping track of verification state it will be necessary anyway. For example even something as simple as having one email and one phone number associated with a user. Or beyond that one user having multiple email addresses or multiple phone numbers etc.
My point was specifically about complexity. Using a string for all kind of emails of course works, tying guarantees to other things like VerifiedUser works as well. These implicit guarantees just fail to safe your ass once things get more complex and there is an edge case you didn't think of.
In your own example, if you happen to use e-mail verification as proxy for user verification, then the very code that creates VerifiedUser instances would want to have VerifiedEmail as input!
Getting hung up on the details of which type you use is missing the overall point.
Your state machine analogy is a good one, because the conversion between different types is a bit like the transitions between states: you have to make explicit how exactly they are meant to happen (or whether they are actually possible).
This is a good thing, having less degrees of freedom may seem like it makes it harder to code, but in fact it allows you to reason better about what the system is doing at any given point in your code.