Transforming TypeScript Flexibility: Harnessing the Power of the Map Pattern

Explore how to enhance flexibility in TypeScript design using the Map pattern. Learn to improve the adaptability of your code while maintaining control with best practices.
Transforming TypeScript Flexibility: Harnessing the Power of the Map Pattern

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: Utilizes Record<string, Reaction> to allow any string as a valid key with a Reaction type value.
  • FinalResponse: Transforms the reactions field into ReactionMap, 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

Visual 1

Visual 2

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.