This is the third time class has been problematic. But now prototype is not involved. The
syntax itself is making it hard to do refactoring with it, especially when
layered lifetime is involved.
Did you just make up a new word?
Yes. Layered Lifetime, I Just Made Up That Phrase
Lifetime is literally when an object lives---the period between its creation and its deletion from the memory.
Specifically in TypeScript/JavaScript, an object's lifetime ends when nothing refers to it---when there are no references left; subsequently, non-referenced objects are removed by the GC, but the period between the reference count zeroing and the collection does not matter because nothing could be done with said objects.
"Layered lifetime" simply refers to a situation where there are objects whose lifetimes are diverse.
A perfect example of "Layered lifetime" would be a Page that has a Modal which is open only at times.
This example will be written in React.
const Page = () => {
const [modalOpen, setModalOpen] = useState(false);
return (
<Layout>
<Toggle onClick={setModalOpen((open) => !!open)} />
{modalOpen && <Modal />}
</Layout>
);
};
The above code says:
- "A
Pagecontains aLayout, aToggle, and optionally aModal" - "
Toggle, when clicked, flipsmodalOpenfromtruetofalse, and vice versa." - "
Modalis open only whenmodalOpenistrue."
Talking in terms of lifetimes:
Modallives within thePage's lifetime;Modallifetime is shorter than thePage's;Modalcannot exist withoutPage.Pagecan exist withoutModal.
The diversity of the lifetime is apparent here.
There are two objects Modal and Page, and their lifetimes differ.
Contrast this with the relationship of Page and Layout whose lifetime is the same.
Lifetime and Type's Interesting Relationship
Above 6 PM I tend to write very dumb code such as:
class User {
id?: string;
name: string;
address: string;
constructor(
name: string,
address: string
) { this.name = name; this.address = address }
setId(id: string) { this.id = id }
isInvalid() { return this.id === undefined; }
}
// On user input I want to create a "user"
function onUserInput(name: string, address: string) {
return new User(name, address);
}
// The product manager wants to only reserve some Id
// from a centralized-badly-architectured-single-threaded-obviously-strawman-database
// ONLY WHEN the user click a confirmation to "confirm their seriousness in registering"
async function onUserConfirm(user: User) {
user.setId(await reserveIdFromCentralizedDatabase())
return user;
}
// When the user actually submits
async function submitUser(user: User) {
// setId should be called before this
if(user.isInvalid()) throw new Error()
return await submitEntity("user", user);
}
The next morning, logic comes back to me.
I realized that User without id does not make sense.
So I refactored the code into:
class UserData {
public name: string;
public address: string;
constructor(name: string, address: string) {
this.name = name;
this.address = address;
}
}
class User extends UserData {
id: string;
constructor(id: string, userData: UserData) {
this.name = userData.name;
this.address = userData.address;
}
}
// On user input I want to create a "user"
function onUserInput(name: string, address: string) {
return new UserData(name, address);
}
// The product manager wants to only reserve some Id
// from a centralized-badly-architectured-single-threaded-obviously-strawman-database
// ONLY WHEN the user click a confirmation to "confirm their seriousness in registering"
async function onUserConfirm(userData: UserData) {
return new User(await reserveIdFromCentralizedDatabase(), userData);
}
// When the user actually submits
async function submitUser(user: User) {
// IMPORTANT: Now there's no need to call `invalid`
await submitEntity("user", user);
}
What I realize is that a User's lifetime can't outlive its id.
After the refactor, id is not optional.
submitUser does not need to validate the always-valid User.
This way, the code represents entities' lifetimes' relationships more correctly.
Too Many Lines Of Codes
"You claim it is simpler. Why are there more lines of code now?" - some review
Whelp! Such is the price of correctness.
"Why don't you just use type?" - some other review.
That's actually a great idea. Well, it was written in class, so I followed.
In hindsight, a class without methods is wasteful.
type UserData = { name: string; address: string };
type User = UserData & { id: string };
const onUserInput = (name: string, address: string): UserData => ({
name,
address,
});
const onUserConfirm = async (data: UserData): User => ({
...data,
id: await reserveIdFromCentralizedDatabase(),
});
const submitUser = (user: User) => await submitEntity("user", user);
There you go!
Wait! Isn't this what "Functional Programming" looks like (or at least, advertised to look like)? It is one of the "evolutionary phases" of systemic-ts.