Introduction
While working on a real-life project, I came across a particular TypeScript implementation that was functional but lacked flexibility. In this blog, I'll walk you through the problem I encountered, and how I improved the design by making a more dynamic approach using the Map pattern.
Table of Contents
- The problem
- The issue with this approach
- Solution
- Clean code
- More secure solution
- Visual representation
- Conslusion
The problem
I came across this TypeScript type:
// FinalResponse.ts
import { Reaction } from './Reaction'
export type FinalResponse = {
totalScore: number
headingsPenalty: number
sentencesPenalty: number
charactersPenalty: number
wordsPenalty: number
headings: string[]
sentences: string[]
words: string[]
links: { href: string; text: string }[]
exceeded: {
exceededSentences: string[]
repeatedWords: { word: string; count: number }[]
}
reactions: {
likes: Reaction
unicorns: Reaction
explodingHeads: Reaction
raisedHands: Reaction
fire: Reaction
}
}
Additionally, this Reaction
type was defined:
// Reaction.ts
export type Reaction = {
count: number
percentage: number
}
And this was being used in a function like so:
// calculator.ts
export const calculateScore = (
headings: string[],
sentences: string[],
words: string[],
totalPostCharactersCount: number,
links: { href: string; text: string }[],
reactions: {
likes: Reaction
unicorns: Reaction
explodingHeads: Reaction
raisedHands: Reaction
fire: Reaction
},
): FinalResponse => {
// Score calculation logic...
}
The Issue with This Approach
Now, imagine the scenario where the developer needs to add a new reaction (e.g., hearts, claps, etc.).
Given the current setup, they would have to:
- Modify the
FinalResponse.ts
file to add the new reaction type. - Update the
Reaction.ts
type if necessary. - Modify the
calculateScore
function to include the new reaction. - Possibly update other parts of the application that rely on this structure.
So instead of just adding the new reaction in one place, they end up making changes in three or more files, which increases the potential for errors and redundancy. This approach is tightly coupled.
Solution
I came up with a cleaner solution by introducing a more flexible and reusable structure.
// FinalResponse.ts
import { Reaction } from './Reaction'
export type ReactionMap = Record<string, Reaction>
export type FinalResponse = {
totalScore: number
headingsPenalty: number
sentencesPenalty: number
charactersPenalty: number
wordsPenalty: number
headings: string[]
sentences: string[]
words: string[]
links: { href: string; text: string }[]
exceeded: {
exceededSentences: string[]
repeatedWords: { word: string; count: number }[]
}
reactions: ReactionMap
}
Explanation:
-
ReactionMap
: This type usesRecord<string, Reaction>
, which means any string can be a key, and the value will always be of typeReaction
. -
FinalResponse
: Now, the reactions field inFinalResponse
is of typeReactionMap
, allowing you to add any reaction dynamically without having to modify multiple files.
Clean code
In the calculator.ts
file, the function now looks like this:
// calculator.ts
export const calculateScore = (
headings: string[],
sentences: string[],
words: string[],
totalPostCharactersCount: number,
links: { href: string; text: string }[],
reactions: ReactionMap,
): FinalResponse => {
// Score calculation logic...
}
But Wait! We Need Some Control
Although the new solution provides flexibility, it also introduces the risk of adding unchecked reactions, meaning anyone could potentially add any string as a reaction. We definitely don't want that.
To fix this, we can enforce stricter control over the allowed reactions.
More secure solution
Here’s the updated version where we restrict the reactions to a predefined set of allowed values:
// FinalResponse.ts
import { Reaction } from './Reaction'
type AllowedReactions =
| 'likes'
| 'unicorns'
| 'explodingHeads'
| 'raisedHands'
| 'fire'
export type ReactionMap = {
[key in AllowedReactions]: Reaction
}
export type FinalResponse = {
totalScore: number
headingsPenalty: number
sentencesPenalty: number
charactersPenalty: number
wordsPenalty: number
headings: string[]
sentences: string[]
words: string[]
links: { href: string; text: string }[]
exceeded: {
exceededSentences: string[]
repeatedWords: { word: string; count: number }[]
}
reactions: ReactionMap
}
Visual representation
Conclusion
This approach strikes a balance between flexibility and control:
-
Flexibility: You can easily add new reactions by modifying just the
AllowedReactions
type. - Control: The use of a union type ensures that only the allowed reactions can be used, preventing the risk of invalid or unwanted reactions being added.
This code follows the Open/Closed Principle (OCP) by enabling the addition of new functionality through extensions, without the need to modify the existing code.
With this pattern, we can easily extend the list of reactions without modifying too many files, while still maintaining strict control over what can be added.
Code?
You can visit the repository here.
Hope you found this solution helpful! Thanks for reading. 😊
Top comments (28)
There are bunch of bad codes there too. For example you can just use Record instead of
You should also separate links according to your article
This is better. Thanks!
good luck mate. Dont stop writing
Plus you can use interface for objects instead of types
I used type because it is immutable. Unlike interface, type cannot be extended later in the code and I wanted to ensure that
ReactionMap
structure remains same and does not change somewhere else in the code. Thanks for the commentIt's not actually accurate I'm afraid do a little research
Hey. Care to explain why we should use interface instead of type?
I said you CAN not you SHOULD
But wait, at some point, we need to give the user a list of available reactions, right? So we already have an array containing them. We should use that to construct our map:
Hi, Alex. Yes, that is possible, if I fetch them from the database for example. But what when not? For case when I get the reactions list from the user?
For example: If I used Caido tool (security auditing toolkit) and append some non existing reaction, in your case, it would be added to the
ReactionMap
without check. That would require manual checking orAllowedReactions
.In my case the
AllowedReactions
are the predefined reactions available onDev.to
, and they are specified in theFinalResponse
type. The list of reactions is passed to the user through the reactions property.This is, I would say, a more advanced project with a lot of data parsing and calculation processes.
In this project, the reactions received on certain
dev.to
blog post are mapped and then the structure like this is created:If you are interested you can check the code in the repository.
You can also try it out here: dev-to-rater.xyz
If you add another reaction to the array of existing reactions, it is obviously an existing reaction itself. Saying the types are more true than your data is a fallacy.
A single source of truth reduces the chances of errors and saves time if you have to change something.
Good post, thanks for sharing! 😃👍🏻
You're welcome! If you want to view it in a real action, feel free to do it in the repository.
Specifically, the files are:
Thanks for reading :)
It's absolutely correct, and it's very easy to see which properties are in this, manageable
Gonna use this
Thanks for feedback. Glad it was useful
Cool! I was doing this without even acknowledging it. It's nice to give it a name (Map Pattern) so it feels solid. Definetely something everyone should know and apply.
I usually kind of skip the "control" part and just use
Record<string, Whatever>
and forget about it. I don't even use the extra typeReactionMap
, I just straight goAlthough yeah, for bigger projects the control should be included without a doubt.
Nice post my man.
Thanks for the comment and feedback man. Yes, it sounds solid. This is a problem that many people get wrong because they don't think about extensibility.
Nice
Glad you liked it
Good post very helpful and thanks to all the people in the comments that are suggesting and improving it
Yes, that is key. To help each other
Great one!
Thank you!
Thanks, this a great tip!
You're welcome!
You left some room for improvements:
How about
Some comments have been hidden by the post's author - find out more