
Navigating Real-World Flexibility in TypeScript
In the dynamic realm of software development, encountering functional yet inflexible code is not uncommon. Recently, I stumbled upon a TypeScript implementation that served its purpose but offered little room for adaptability. In this blog, I’ll guide you through my journey of resolving this issue by adopting a more dynamic solution using the Map pattern.
Unveiling the Rigid Structure
Confronted with this particular TypeScript type, I realized its rigidity:
// 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
}
}
This setup relied on a companion Reaction
type:
// Reaction.ts
export type Reaction = {
count: number
percentage: number
}
And it was engaged in the following function:
// 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 Inflexibility Dilemma
Imagine the scenario where you need to integrate a new reaction (e.g., hearts or claps). With the existing structure, introducing this seemingly minor change involves:
- Overhauling the
FinalResponse.ts
file. - Updating the
Reaction.ts
type if necessary. - Modifying the
calculateScore
function. - Potentially redrafting other reliant sections of the application.
Such tight coupling disallows simple, direct changes and inflates error potential across multiple files.
A Dynamic Redesign
By reimagining the setup with a flexible and reusable approach, I proposed an elegant redesign:
// 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
}
Breaking It Down:
ReactionMap
: UtilizesRecord<string, Reaction>
to allow any string as a valid key with aReaction
type value.FinalResponse
: Transforms thereactions
field intoReactionMap
, enabling dynamic addition without delving into multiple files.
Advocating for Cleaner Code
The transformation in calculator.ts
stands as a testament to cleaner, more adaptable coding:
// calculator.ts
export const calculateScore = (
headings: string[],
sentences: string[],
words: string[],
totalPostCharactersCount: number,
links: { href: string; text: string }[],
reactions: ReactionMap,
): FinalResponse => {
// Score calculation logic...
}
Balancing Flexibility with Control
While newfound flexibility offers vast possibilities, it teeters on the edge of potential misuseāintroducing unchecked reactions might pave the way for arbitrary strings.
Striking a Secure Balance
Refining the solution by constraining reactions to specific allowable values enhances security and consistency:
// 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
}
Envisioning the New Paradigm
Concluding the Journey
Here lies an approach that harmonizes flexibility with control:
- Flexibility: Simplify adding new reactions by altering just the
AllowedReactions
type. - Control: Leveraging a union type guards against the inclusion of invalid reactions.
This methodology aligns with the Open/Closed Principle, enabling functionality extension without modifying core structures. Through this innovative pattern, we extend reaction types effortlessly while maintaining governance over permissible actions.