As it turns out, you have to earn TypeScript by fixing technical debt
For my birthday, my partner agreed to teach me TypeScript. This came with some surprise strings attached, like “stop circumventing React”. So first, instead of using custom attributes I could read out of an event, I had to change my code to use proper parameters in the callback function associated with the event handler.
I will do some more lines later, but I’ll start my penance now:
I will not use the DOM as a state store, nor will I bypass the use of state and props.
If I want my own special state store pile, I must use a real one like Redux. The builtin state store that comes with React is perfectly fine as well. The way data in memory is meant to be stored and used is in state, passed as a property to a component. Not hidden in the DOM and plucked out of a click event object.
If I had two functions that conditionally modified the data I had previously stored in the DOM, I wouldn’t cause this data to actually propagate since React wouldn’t queue a new render pass. While that would have been unlikely given that I was storing ID values unlikely to change, if I get into this as a long term habit I’ll apparently cause “nightmare debugging sessions” and “make the senior developers grumpy”.
KonMari: Evading React Methodology
The first thing to learn is about not evading Javascript and React’s builtin features. I do not need to set custom attributes on this <img>
tag. The best practice here is actually to pass the face value (card.id
) and the individual value (card.flippedid
) as arguments to the event handler’s callback function.
From This… | … To this! |
---|---|
return ( <img className="card" cardid={card.id} //adds card value to event for handleflip flippedid={card.flippedid} //adds card id to event for handleflip src={flipped ? card.image : cardBack} alt={card.alt} onClick={ flipped ? null : (event) => { handleFlip(event); } } /> ); functionhandleFlip(cardClickEvent,) { let clickedCardId = cardClickEvent.target.attributes.cardid.value; let clickedCardFlippedId = cardClickEvent.target.attributes.flippedid.value; | return ( <img className="card" src={flipped ? card.image : cardBack} alt={card.alt} onClick={ flipped ? null : (event) => { handleFlip(event, card.id, card.flippedid); } } /> ); functionhandleFlip(cardClickEvent, faceID, valueID) { let clickedCardId = faceID; let clickedCardFlippedId = valueID; |
Making this work also required changing some of the way the data of a card object is stored. In the JSON data file, the card’s face value ID is stored as a string. We literally never use this value as a string anywhere in this project. It makes more sense to store as an integer and remove the .toString()
methods from this code block:
if (flipped === false) {
if (faceUpCards.includes(card.flippedid.toString())) {
setFlipped(true);
} else {
Now, we save precious compute cycles for literally anything else by directly comparing integers without type conversion:
if (flipped === false) {
if (faceUpCards.includes(card.flippedid)) {
setFlipped(true);
} else {
}
}
if (flipped === true) {
if (faceUpCards.includes(card.flippedid)) {
} else {
setFlipped(false);
}
}
Having to deal with typed comparisons in standard JavaScript is part of what got us into this mess. That and my stalwart refusal to store more on disk than is necessary, no matter that I probably only saved something like 6kB.
Firstly, let’s head back to our shell and install the dependencies that allow us to use TypeScript in this legacy code. Since by this time my partner needed to go back to work for a prod deploy, I’ll be following a guide I found on Jayson Alzate’s blog.
npm install --save-dev typescript @typescript-eslint/parser @typescript-eslint/eslint-plugin ts-loader
Next, I added a tsconfig.json
file to my document root. I’m using this one from Jayson’s post since it doesn’t enforce strict typing, and that makes it good for legacy codebases in transition.
After that, I’ll need to check my package.json file to make sure everything worked and it will load .ts
and .tsx
files, which will enable me to convert that comparison above to a strictly typed method.
Thanks to the npm
command above, we do see that package.json
has already been updated accordingly:
"devDependencies":{"@typescript-eslint/eslint-plugin":"^5.41.0","@typescript-eslint/parser":"^5.41.0","ts-loader":"^9.4.1","typescript":"^4.8.4"}
Using a strictly typed method will save time. I remember that my group spent over an hour trying to figure out why the comparison didn’t work as we expected in the first place, but TypeScript would throw a type error if we had used it. Also, the solution was not to just typecast at runtime.
The guide I’m referencing uses a .tsx
file as an entry point, but I’m not ready to change that yet, nor am I ready to do much more in-depth refactoring. Perhaps I’ll actually make more forays into TypeScript with a brand-new project, rather than learning by refactoring. In any case, learning how to go back and re-assess legacy code is a great skill and I’m working on cultivating it so that I can improve as a programmer.
Maybe one day soon, I’ll earn my TypeScript privileges!
Thank you for reading. If you liked this article, have some suggestion to make, or have further questions, please feel free to drop me a comment below.
Word Count:
835