Comment by throwaw12
6 hours ago
I personally love the idea and concept, but struggle to apply to real projects.
Suppose I have a User with some attributes like birthday, email and whether they have been verified.
in common codebase, you can see `if (user.verified_at != null)` or something along the lines, in case of parsed code I do feel like I should have types for each of them (or interfaces):
- UserWithBirthday
- VerifiedUser, UnverifiedUser
- UserWithEmail, UserWithoutEmail
(and imagine having a method which accepts user with birthday and email to send an email day before their birthday, would you create UserWithBirthdayAndEmail type?)
it feels like it is going to bloat the interface space, how do you tackle this problem?
It's pretty trivial to create derived and augmented types with Pick, Omit, Required, Partial. Combined with a few parsing functions that return an object typed to whatever specification you need and you are set IE:
Any caller of userHasBDayAndEmail knows for the rest of its nested call stack if the provided user is a User object or a VerifiedUserWithBirthday.
The types are cheap to write (they're all derived) and have no runtime impact (types are erased at build/compile time) and these parsing functions are quite small to write
https://www.typescriptlang.org/play/?#code/FAFwngDgpgBAqgZyg...
creation is not a problem, maintenance is.
Suppose you want to add one more property to VerifiedUserWithBirthday and UnverifiedUserWithBirthday, you might get 2 more new types, and somewhere at the higher layer call chains you need to know which enclosing type you should pass so that some method in the bottom chain will accept it.
I am sure there are more elegant ways, but I am struggling to generalize it to most enterprise SaaS CRUD apps, where you have one object with bunch of properties and can conditionally traverse the code logic
Yeah that's the engineering part in software engineer :)
If you have VerifiedUserWithBirthday, any value that fails the parsing function is implicitly UnverifiedUserOrUserWithoutBirthday... No need to define it separately. You get the inverse type for free IE a value that is of type User and not of type VerifiedUserWithBirthday.
A new property doesn't mean a new derived type. Only if that new property impacts what a VerifiedUserWithBirthday should represent should the VerifiedUserWithBirthday type be updated and even then, it's not a new type, just an update to an existing type. Again minimal updates needed.
The compiler handles all the validation and will tell you exactly where there are any issues - the compiler is what makes the maintenance cost quite low.
I think this is the wrong pattern in this instance. You parse an email or phone number because validating leaves it as a plain string, and you lose the context to know for sure if that string is actually an email or phone number.
In your instance, you could have:
In this instance, your logic with a method that accepts birthday and email has all the information it needs to make its choice.
The computer-science answer to this problem are called "refinement types", where you can attach arbitrary predicates to a type, e.g. (pseudo-code):
Contracts are a similar solution that restricts the predicates to only appearing in function types.
The difference between this and an assert is that it gets checked at compile time (it can get quite expensive to do the check though).
What can you do in mainstream languages? As much as is worth and no more than that. String -> User is worth it, User -> UserWithBirthday is not.
this looks cool, but you are doing validation when accepting the object, you probably can't do it excessively, for example, if you are dealing with objects with heights, you might have a HumanLikeHeight where height range is between 40cm and 250cm, and you want to send email to that human, would you keep adding these conditions to the predicates?
Languages with refinement types (or contracts) like Dafny and Liquid Haskell can typically handle numerical predicates directly. Some can even handle string predicates directly, including regular expressions. They also allow you to write complex predicates as separate functions, albeit with limited expressiveness.
But you hit performance and/or outright computational limits (halting problem) rather quickly.
> Suppose I have a User with some attributes like birthday, email and whether they have been verified.
Philosophically, birthday and email are not attributes of a user. If you remove a user from existence, a birthdate and email address still exist. So...
> would you create UserWithBirthdayAndEmail type
...yes, something like a `profile { user, birthday, email }` type is necessary to compose the attributes you are interested in into something where those attributes do belong together.
> it feels like it is going to bloat the interface space, how do you tackle this problem?
Like all things formal verification, increase the level of verification in your critical sections and don't sweat the non-critical sections. How impactful will it be to your business if sending a birthday email message fails?