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:
type User = { name: string; verified: boolean; email?: string; lastName: string; birthday?: string | { year: string; month: string; date: string; }}
type Birthday = Required<Pick<User, 'birthday'>>;
type UserWithBirthday = User & { birthday: Birthday }
type VerifiedUser = User & { verified: true; email: string; }
type VerifiedUserWithBirthday = User & UserWithBirthday & VerifiedUser;
const userHasBDayAndEmail = (user: User): user is VerifiedUserWithBirthday => {
if (user.email === undefined || user.birthday === undefined) {
return false
}
return true
}
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