DEV Community

Cover image for Don't use TypeScript types like this. Use Map Pattern instead
Nikola Perišić
Nikola Perišić

Posted on

Don't use TypeScript types like this. Use Map Pattern instead

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

  1. The problem
  2. The issue with this approach
  3. Solution
  4. Clean code
  5. More secure solution
  6. Visual representation
  7. 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
  }
}
Enter fullscreen mode Exit fullscreen mode

Additionally, this Reaction type was defined:

// Reaction.ts
export type Reaction = {
  count: number
  percentage: number
}
Enter fullscreen mode Exit fullscreen mode

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...
}

Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • ReactionMap: This type uses Record<string, Reaction>, which means any string can be a key, and the value will always be of type Reaction.
  • FinalResponse: Now, the reactions field in FinalResponse is of type ReactionMap, 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...
}
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

Visual representation

TypeScript Types

TypeScript Types

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.

Hope you found this solution helpful! Thanks for reading. 😊

Top comments (4)

Collapse
 
joelbonetr profile image
JoelBonetR 🥇

Good post, thanks for sharing! 😃👍🏻

Collapse
 
perisicnikola37 profile image
Nikola Perišić • Edited

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 :)

Collapse
 
wizard798 profile image
Wizard

It's absolutely correct, and it's very easy to see which properties are in this, manageable
Gonna use this

Collapse
 
perisicnikola37 profile image
Nikola Perišić

Thanks for feedback. Glad it was useful